Compare commits
68 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 | ||
|
|
42e2e3463e | ||
|
|
2ef54bae36 | ||
|
|
6766a8120a | ||
|
|
9fcf1e7164 | ||
|
|
130c9bd696 | ||
|
|
88db920ebe | ||
|
|
5278805d79 | ||
|
|
55863f849c | ||
|
|
0590facc89 | ||
|
|
3ff339ba1d | ||
|
|
e92a1d8a4d | ||
|
|
2590f5612c | ||
|
|
112685bdf4 | ||
|
|
7e1455161d | ||
|
|
be98b77634 | ||
|
|
cb75923f9f | ||
|
|
87ab4961fd | ||
|
|
d053f16058 | ||
|
|
19272c05bf | ||
|
|
e56c5f30e0 | ||
|
|
7cf7b706c1 | ||
|
|
58fe9f723a | ||
|
|
a337ece52e | ||
|
|
61723a24e0 | ||
|
|
b7934931cf | ||
|
|
0566433aa1 | ||
|
|
b5607025f7 | ||
|
|
683dc74cbf | ||
|
|
c7e67a9b63 | ||
|
|
083da9598e | ||
|
|
f8d2161489 | ||
|
|
d0d4e546d9 | ||
|
|
2ac5d797d2 | ||
|
|
4105567051 | ||
|
|
c06eabefe0 | ||
|
|
9d2192f323 | ||
|
|
dd55e74ec9 | ||
|
|
9da1e5751a | ||
|
|
ea71492d13 | ||
|
|
b51770a703 | ||
|
|
39596b8da8 | ||
|
|
932d126117 | ||
|
|
034a5c21eb | ||
|
|
d840178cc0 |
15
.github/workflows/docker-images.yml
vendored
@@ -13,11 +13,11 @@ jobs:
|
|||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
- image: henrygd/beszel
|
- image: henrygd/beszel
|
||||||
context: ./hub
|
context: ./beszel
|
||||||
dockerfile: ./hub/Dockerfile
|
dockerfile: ./beszel/dockerfile_Hub
|
||||||
- image: henrygd/beszel-agent
|
- image: henrygd/beszel-agent
|
||||||
context: ./agent
|
context: ./beszel
|
||||||
dockerfile: ./agent/Dockerfile
|
dockerfile: ./beszel/dockerfile_Agent
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
packages: write
|
packages: write
|
||||||
@@ -30,10 +30,10 @@ jobs:
|
|||||||
uses: oven-sh/setup-bun@v2
|
uses: oven-sh/setup-bun@v2
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: bun install --no-save --cwd ./hub/site
|
run: bun install --no-save --cwd ./beszel/site
|
||||||
|
|
||||||
- name: Build site
|
- name: Build site
|
||||||
run: bun run --cwd ./hub/site build
|
run: bun run --cwd ./beszel/site build
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v3
|
uses: docker/setup-qemu-action@v3
|
||||||
@@ -49,6 +49,7 @@ jobs:
|
|||||||
tags: |
|
tags: |
|
||||||
type=semver,pattern={{version}}
|
type=semver,pattern={{version}}
|
||||||
type=semver,pattern={{major}}.{{minor}}
|
type=semver,pattern={{major}}.{{minor}}
|
||||||
|
type=semver,pattern={{major}}
|
||||||
type=raw,value={{sha}},enable=${{ github.ref_type != 'tag' }}
|
type=raw,value={{sha}},enable=${{ github.ref_type != 'tag' }}
|
||||||
|
|
||||||
# https://github.com/docker/login-action
|
# https://github.com/docker/login-action
|
||||||
@@ -66,7 +67,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
context: '${{ matrix.context }}'
|
context: '${{ matrix.context }}'
|
||||||
file: ${{ matrix.dockerfile }}
|
file: ${{ matrix.dockerfile }}
|
||||||
platforms: linux/amd64,linux/arm64
|
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||||
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 }}
|
||||||
|
|||||||
@@ -21,10 +21,10 @@ jobs:
|
|||||||
uses: oven-sh/setup-bun@v2
|
uses: oven-sh/setup-bun@v2
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: bun install --no-save --cwd ./hub/site
|
run: bun install --no-save --cwd ./beszel/site
|
||||||
|
|
||||||
- name: Build site
|
- name: Build site
|
||||||
run: bun run --cwd ./hub/site build
|
run: bun run --cwd ./beszel/site build
|
||||||
|
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
@@ -34,17 +34,7 @@ jobs:
|
|||||||
- name: GoReleaser beszel
|
- name: GoReleaser beszel
|
||||||
uses: goreleaser/goreleaser-action@v6
|
uses: goreleaser/goreleaser-action@v6
|
||||||
with:
|
with:
|
||||||
workdir: ./hub
|
workdir: ./beszel
|
||||||
distribution: goreleaser
|
|
||||||
version: latest
|
|
||||||
args: release --clean
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.TOKEN }}
|
|
||||||
|
|
||||||
- name: GoReleaser beszel-agent
|
|
||||||
uses: goreleaser/goreleaser-action@v6
|
|
||||||
with:
|
|
||||||
workdir: ./agent
|
|
||||||
distribution: goreleaser
|
distribution: goreleaser
|
||||||
version: latest
|
version: latest
|
||||||
args: release --clean
|
args: release --clean
|
||||||
8
.gitignore
vendored
@@ -3,8 +3,12 @@ pb_data
|
|||||||
data
|
data
|
||||||
temp
|
temp
|
||||||
.vscode
|
.vscode
|
||||||
beszel
|
|
||||||
beszel-agent
|
beszel-agent
|
||||||
beszel_data
|
beszel_data
|
||||||
beszel_data*
|
beszel_data*
|
||||||
dist
|
dist
|
||||||
|
*.exe
|
||||||
|
beszel/cmd/hub/hub
|
||||||
|
beszel/cmd/agent/agent
|
||||||
|
node_modules
|
||||||
|
package-lock.json
|
||||||
|
|||||||
@@ -1,42 +0,0 @@
|
|||||||
# version: 1
|
|
||||||
|
|
||||||
project_name: beszel-agent
|
|
||||||
|
|
||||||
before:
|
|
||||||
hooks:
|
|
||||||
- go mod tidy
|
|
||||||
|
|
||||||
builds:
|
|
||||||
- env:
|
|
||||||
- CGO_ENABLED=0
|
|
||||||
goos:
|
|
||||||
- linux
|
|
||||||
- darwin
|
|
||||||
- freebsd
|
|
||||||
goarch:
|
|
||||||
- amd64
|
|
||||||
- arm64
|
|
||||||
- arm
|
|
||||||
- mips64
|
|
||||||
ignore:
|
|
||||||
- goos: freebsd
|
|
||||||
goarch: arm
|
|
||||||
|
|
||||||
archives:
|
|
||||||
- format: tar.gz
|
|
||||||
name_template: >-
|
|
||||||
{{ .ProjectName }}_
|
|
||||||
{{- .Os }}_
|
|
||||||
{{- .Arch }}
|
|
||||||
# use zip for windows archives
|
|
||||||
format_overrides:
|
|
||||||
- goos: windows
|
|
||||||
format: zip
|
|
||||||
|
|
||||||
changelog:
|
|
||||||
disable: true
|
|
||||||
sort: asc
|
|
||||||
filters:
|
|
||||||
exclude:
|
|
||||||
- '^docs:'
|
|
||||||
- '^test:'
|
|
||||||
30
agent/go.mod
@@ -1,30 +0,0 @@
|
|||||||
module beszel-agent
|
|
||||||
|
|
||||||
go 1.22.4
|
|
||||||
|
|
||||||
require (
|
|
||||||
github.com/blang/semver v3.5.1+incompatible
|
|
||||||
github.com/gliderlabs/ssh v0.3.7
|
|
||||||
github.com/rhysd/go-github-selfupdate v1.2.3
|
|
||||||
github.com/shirou/gopsutil/v4 v4.24.7
|
|
||||||
)
|
|
||||||
|
|
||||||
require (
|
|
||||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect
|
|
||||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
|
||||||
github.com/google/go-github/v30 v30.1.0 // indirect
|
|
||||||
github.com/google/go-querystring v1.1.0 // indirect
|
|
||||||
github.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf // indirect
|
|
||||||
github.com/lufia/plan9stats v0.0.0-20240513124658-fba389f38bae // indirect
|
|
||||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
|
|
||||||
github.com/shoenig/go-m1cpu v0.1.6 // indirect
|
|
||||||
github.com/tcnksm/go-gitconfig v0.1.2 // indirect
|
|
||||||
github.com/tklauser/go-sysconf v0.3.14 // indirect
|
|
||||||
github.com/tklauser/numcpus v0.8.0 // indirect
|
|
||||||
github.com/ulikunitz/xz v0.5.12 // indirect
|
|
||||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
|
||||||
golang.org/x/crypto v0.26.0 // indirect
|
|
||||||
golang.org/x/net v0.27.0 // indirect
|
|
||||||
golang.org/x/oauth2 v0.22.0 // indirect
|
|
||||||
golang.org/x/sys v0.23.0 // indirect
|
|
||||||
)
|
|
||||||
100
agent/go.sum
@@ -1,100 +0,0 @@
|
|||||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
|
|
||||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
|
|
||||||
github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ=
|
|
||||||
github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
|
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
|
||||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
|
||||||
github.com/gliderlabs/ssh v0.3.7 h1:iV3Bqi942d9huXnzEF2Mt+CY9gLu8DNM4Obd+8bODRE=
|
|
||||||
github.com/gliderlabs/ssh v0.3.7/go.mod h1:zpHEXBstFnQYtGnB8k8kQLol82umzn/2/snG7alWVD8=
|
|
||||||
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
|
||||||
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
|
||||||
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
|
||||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
|
||||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
|
||||||
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
|
||||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
|
||||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
|
||||||
github.com/google/go-github/v30 v30.1.0 h1:VLDx+UolQICEOKu2m4uAoMti1SxuEBAl7RSEG16L+Oo=
|
|
||||||
github.com/google/go-github/v30 v30.1.0/go.mod h1:n8jBpHl45a/rlBUtRJMOG4GhNADUQFEufcolZ95JfU8=
|
|
||||||
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/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
|
|
||||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
|
||||||
github.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf h1:WfD7VjIE6z8dIvMsI4/s+1qr5EL+zoIGev1BQj1eoJ8=
|
|
||||||
github.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf/go.mod h1:hyb9oH7vZsitZCiBt0ZvifOrB+qc8PS5IiilCIb87rg=
|
|
||||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
|
||||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
|
||||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
|
||||||
github.com/lufia/plan9stats v0.0.0-20240513124658-fba389f38bae h1:dIZY4ULFcto4tAFlj1FYZl8ztUZ13bdq+PLY+NOfbyI=
|
|
||||||
github.com/lufia/plan9stats v0.0.0-20240513124658-fba389f38bae/go.mod h1:ilwx/Dta8jXAgpFYFvSWEMwxmbWXyiUHkd5FwyKhb5k=
|
|
||||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
|
||||||
github.com/onsi/gomega v1.4.2 h1:3mYCb7aPxS/RU7TI1y4rkEn1oKmPRjNJLNEXgw7MH2I=
|
|
||||||
github.com/onsi/gomega v1.4.2/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
|
||||||
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/rhysd/go-github-selfupdate v1.2.3 h1:iaa+J202f+Nc+A8zi75uccC8Wg3omaM7HDeimXA22Ag=
|
|
||||||
github.com/rhysd/go-github-selfupdate v1.2.3/go.mod h1:mp/N8zj6jFfBQy/XMYoWsmfzxazpPAODuqarmPDe2Rg=
|
|
||||||
github.com/shirou/gopsutil/v4 v4.24.7 h1:V9UGTK4gQ8HvcnPKf6Zt3XHyQq/peaekfxpJ2HSocJk=
|
|
||||||
github.com/shirou/gopsutil/v4 v4.24.7/go.mod h1:0uW/073rP7FYLOkvxolUQM5rMOLTNmRXnFKafpb71rw=
|
|
||||||
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/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
|
||||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
|
||||||
github.com/tcnksm/go-gitconfig v0.1.2 h1:iiDhRitByXAEyjgBqsKi9QU4o2TNtv9kPP3RgPgXBPw=
|
|
||||||
github.com/tcnksm/go-gitconfig v0.1.2/go.mod h1:/8EhP4H7oJZdIPyT+/UIsG87kTzrzM4UsLGSItWYCpE=
|
|
||||||
github.com/tklauser/go-sysconf v0.3.14 h1:g5vzr9iPFFz24v2KZXs/pvpvh8/V9Fw6vQK5ZZb78yU=
|
|
||||||
github.com/tklauser/go-sysconf v0.3.14/go.mod h1:1ym4lWMLUOhuBOPGtRcJm7tEGX4SCYNEEEtghGG/8uY=
|
|
||||||
github.com/tklauser/numcpus v0.8.0 h1:Mx4Wwe/FjZLeQsK/6kt2EOepwwSl7SmJrK5bV/dXYgY=
|
|
||||||
github.com/tklauser/numcpus v0.8.0/go.mod h1:ZJZlAY+dmR4eut8epnzf0u/VwodKmryxR8txiloSqBE=
|
|
||||||
github.com/ulikunitz/xz v0.5.9/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
|
||||||
github.com/ulikunitz/xz v0.5.12 h1:37Nm15o69RwBkXM0J6A5OlE67RZTfzUxTj8fB3dfcsc=
|
|
||||||
github.com/ulikunitz/xz v0.5.12/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
|
||||||
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
|
||||||
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
|
||||||
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
|
||||||
golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw=
|
|
||||||
golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
|
|
||||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
|
||||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
|
||||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
|
||||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
|
||||||
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
|
|
||||||
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
|
|
||||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
|
||||||
golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
|
||||||
golang.org/x/oauth2 v0.22.0 h1:BzDx2FehcG7jJwgWLELCdmLuxk2i+x9UDpSiss2u0ZA=
|
|
||||||
golang.org/x/oauth2 v0.22.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
|
||||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
|
||||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM=
|
|
||||||
golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
|
||||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
|
||||||
golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU=
|
|
||||||
golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk=
|
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
|
||||||
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
|
||||||
golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc=
|
|
||||||
golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
|
||||||
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=
|
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
|
||||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
|
||||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
|
||||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
|
||||||
gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE=
|
|
||||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
|
||||||
480
agent/main.go
@@ -1,480 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"log"
|
|
||||||
"math"
|
|
||||||
"net"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
sshServer "github.com/gliderlabs/ssh"
|
|
||||||
|
|
||||||
"github.com/shirou/gopsutil/v4/cpu"
|
|
||||||
"github.com/shirou/gopsutil/v4/disk"
|
|
||||||
"github.com/shirou/gopsutil/v4/host"
|
|
||||||
"github.com/shirou/gopsutil/v4/mem"
|
|
||||||
psutilNet "github.com/shirou/gopsutil/v4/net"
|
|
||||||
)
|
|
||||||
|
|
||||||
var Version = "0.1.2"
|
|
||||||
|
|
||||||
var containerStatsMap = make(map[string]*PrevContainerStats)
|
|
||||||
var containerStatsMutex = &sync.Mutex{}
|
|
||||||
|
|
||||||
var sem = make(chan struct{}, 15)
|
|
||||||
|
|
||||||
func acquireSemaphore() {
|
|
||||||
sem <- struct{}{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func releaseSemaphore() {
|
|
||||||
<-sem
|
|
||||||
}
|
|
||||||
|
|
||||||
var diskIoStats = DiskIoStats{
|
|
||||||
Read: 0,
|
|
||||||
Write: 0,
|
|
||||||
Time: time.Now(),
|
|
||||||
Filesystem: "",
|
|
||||||
}
|
|
||||||
|
|
||||||
var netIoStats = NetIoStats{
|
|
||||||
BytesRecv: 0,
|
|
||||||
BytesSent: 0,
|
|
||||||
Time: time.Now(),
|
|
||||||
Name: "",
|
|
||||||
}
|
|
||||||
|
|
||||||
// client for docker engine api
|
|
||||||
var dockerClient = newDockerClient()
|
|
||||||
|
|
||||||
func getSystemStats() (*SystemInfo, *SystemStats) {
|
|
||||||
systemStats := &SystemStats{}
|
|
||||||
|
|
||||||
// cpu percent
|
|
||||||
cpuPct, err := cpu.Percent(0, false)
|
|
||||||
if err != nil {
|
|
||||||
log.Println("Error getting cpu percent:", err)
|
|
||||||
} else if len(cpuPct) > 0 {
|
|
||||||
systemStats.Cpu = twoDecimals(cpuPct[0])
|
|
||||||
}
|
|
||||||
|
|
||||||
// memory
|
|
||||||
if v, err := mem.VirtualMemory(); err == nil {
|
|
||||||
systemStats.Mem = bytesToGigabytes(v.Total)
|
|
||||||
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.SwapUsed = bytesToGigabytes(v.SwapTotal - v.SwapFree)
|
|
||||||
}
|
|
||||||
|
|
||||||
// disk usage
|
|
||||||
if d, err := disk.Usage("/"); err == nil {
|
|
||||||
systemStats.Disk = bytesToGigabytes(d.Total)
|
|
||||||
systemStats.DiskUsed = bytesToGigabytes(d.Used)
|
|
||||||
systemStats.DiskPct = twoDecimals(d.UsedPercent)
|
|
||||||
}
|
|
||||||
|
|
||||||
// disk i/o
|
|
||||||
if io, err := disk.IOCounters(diskIoStats.Filesystem); err == nil {
|
|
||||||
for _, d := range io {
|
|
||||||
// add to systemStats
|
|
||||||
secondsElapsed := time.Since(diskIoStats.Time).Seconds()
|
|
||||||
readPerSecond := float64(d.ReadBytes-diskIoStats.Read) / secondsElapsed
|
|
||||||
systemStats.DiskRead = bytesToMegabytes(readPerSecond)
|
|
||||||
writePerSecond := float64(d.WriteBytes-diskIoStats.Write) / secondsElapsed
|
|
||||||
systemStats.DiskWrite = bytesToMegabytes(writePerSecond)
|
|
||||||
// update diskIoStats
|
|
||||||
diskIoStats.Time = time.Now()
|
|
||||||
diskIoStats.Read = d.ReadBytes
|
|
||||||
diskIoStats.Write = d.WriteBytes
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// network stats
|
|
||||||
if netIO, err := psutilNet.IOCounters(true); err == nil {
|
|
||||||
bytesSent := uint64(0)
|
|
||||||
bytesRecv := uint64(0)
|
|
||||||
for _, v := range netIO {
|
|
||||||
if skipNetworkInterface(&v) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
// log.Printf("%+v: %+v recv, %+v sent\n", v.Name, v.BytesRecv, v.BytesSent)
|
|
||||||
bytesSent += v.BytesSent
|
|
||||||
bytesRecv += v.BytesRecv
|
|
||||||
}
|
|
||||||
// add to systemStats
|
|
||||||
secondsElapsed := time.Since(netIoStats.Time).Seconds()
|
|
||||||
sentPerSecond := float64(bytesSent-netIoStats.BytesSent) / secondsElapsed
|
|
||||||
recvPerSecond := float64(bytesRecv-netIoStats.BytesRecv) / secondsElapsed
|
|
||||||
systemStats.NetworkSent = bytesToMegabytes(sentPerSecond)
|
|
||||||
systemStats.NetworkRecv = bytesToMegabytes(recvPerSecond)
|
|
||||||
// update netIoStats
|
|
||||||
netIoStats.BytesSent = bytesSent
|
|
||||||
netIoStats.BytesRecv = bytesRecv
|
|
||||||
netIoStats.Time = time.Now()
|
|
||||||
}
|
|
||||||
|
|
||||||
systemInfo := &SystemInfo{
|
|
||||||
Cpu: systemStats.Cpu,
|
|
||||||
MemPct: systemStats.MemPct,
|
|
||||||
DiskPct: systemStats.DiskPct,
|
|
||||||
}
|
|
||||||
|
|
||||||
// add host info
|
|
||||||
if info, err := host.Info(); err == nil {
|
|
||||||
systemInfo.Uptime = info.Uptime
|
|
||||||
// systemInfo.Os = info.OS
|
|
||||||
}
|
|
||||||
// add cpu stats
|
|
||||||
if info, err := cpu.Info(); err == nil && len(info) > 0 {
|
|
||||||
systemInfo.CpuModel = info[0].ModelName
|
|
||||||
}
|
|
||||||
if cores, err := cpu.Counts(false); err == nil {
|
|
||||||
systemInfo.Cores = cores
|
|
||||||
}
|
|
||||||
if threads, err := cpu.Counts(true); err == nil {
|
|
||||||
systemInfo.Threads = threads
|
|
||||||
}
|
|
||||||
|
|
||||||
return systemInfo, systemStats
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func getDockerStats() ([]*ContainerStats, error) {
|
|
||||||
resp, err := dockerClient.Get("http://localhost/containers/json")
|
|
||||||
if err != nil {
|
|
||||||
closeIdleConnections(err)
|
|
||||||
return []*ContainerStats{}, err
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
var containers []*Container
|
|
||||||
if err := json.NewDecoder(resp.Body).Decode(&containers); err != nil {
|
|
||||||
log.Printf("Error decoding containers: %+v\n", err)
|
|
||||||
return []*ContainerStats{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
containerStats := make([]*ContainerStats, 0, len(containers))
|
|
||||||
|
|
||||||
// store valid ids to clean up old container ids from map
|
|
||||||
validIds := make(map[string]struct{}, len(containers))
|
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
|
|
||||||
for _, ctr := range containers {
|
|
||||||
ctr.IdShort = ctr.Id[:12]
|
|
||||||
validIds[ctr.IdShort] = struct{}{}
|
|
||||||
// check if container is less than 1 minute old (possible restart)
|
|
||||||
// note: can't use Created field because it's not updated on restart
|
|
||||||
if strings.HasSuffix(ctr.Status, "seconds") {
|
|
||||||
// if so, remove old container data
|
|
||||||
deleteContainerStatsSync(ctr.IdShort)
|
|
||||||
}
|
|
||||||
wg.Add(1)
|
|
||||||
go func() {
|
|
||||||
defer wg.Done()
|
|
||||||
cstats, err := getContainerStats(ctr)
|
|
||||||
if err != nil {
|
|
||||||
// Check if the error is a network timeout
|
|
||||||
if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
|
|
||||||
// Close idle connections to prevent reuse of stale connections
|
|
||||||
closeIdleConnections(err)
|
|
||||||
} else {
|
|
||||||
// otherwise delete container from map
|
|
||||||
deleteContainerStatsSync(ctr.IdShort)
|
|
||||||
}
|
|
||||||
// retry once
|
|
||||||
cstats, err = getContainerStats(ctr)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Error getting container stats: %+v\n", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
containerStats = append(containerStats, cstats)
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
wg.Wait()
|
|
||||||
|
|
||||||
for id := range containerStatsMap {
|
|
||||||
if _, exists := validIds[id]; !exists {
|
|
||||||
// log.Printf("Removing container cpu map entry: %+v\n", id)
|
|
||||||
delete(containerStatsMap, id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return containerStats, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func getContainerStats(ctr *Container) (*ContainerStats, error) {
|
|
||||||
// use semaphore to limit concurrency
|
|
||||||
acquireSemaphore()
|
|
||||||
defer releaseSemaphore()
|
|
||||||
resp, err := dockerClient.Get("http://localhost/containers/" + ctr.IdShort + "/stats?stream=0&one-shot=1")
|
|
||||||
if err != nil {
|
|
||||||
return &ContainerStats{}, err
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
var statsJson CStats
|
|
||||||
if err := json.NewDecoder(resp.Body).Decode(&statsJson); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
name := ctr.Names[0][1:]
|
|
||||||
|
|
||||||
// memory (https://docs.docker.com/reference/cli/docker/container/stats/)
|
|
||||||
memCache := statsJson.MemoryStats.Stats["inactive_file"]
|
|
||||||
if memCache == 0 {
|
|
||||||
memCache = statsJson.MemoryStats.Stats["cache"]
|
|
||||||
}
|
|
||||||
usedMemory := statsJson.MemoryStats.Usage - memCache
|
|
||||||
|
|
||||||
containerStatsMutex.Lock()
|
|
||||||
defer containerStatsMutex.Unlock()
|
|
||||||
|
|
||||||
// add empty values if they doesn't exist in map
|
|
||||||
stats, initialized := containerStatsMap[ctr.IdShort]
|
|
||||||
if !initialized {
|
|
||||||
stats = &PrevContainerStats{}
|
|
||||||
containerStatsMap[ctr.IdShort] = stats
|
|
||||||
}
|
|
||||||
|
|
||||||
// cpu
|
|
||||||
cpuDelta := statsJson.CPUStats.CPUUsage.TotalUsage - stats.Cpu[0]
|
|
||||||
systemDelta := statsJson.CPUStats.SystemUsage - stats.Cpu[1]
|
|
||||||
cpuPct := float64(cpuDelta) / float64(systemDelta) * 100
|
|
||||||
if cpuPct > 100 {
|
|
||||||
return &ContainerStats{}, fmt.Errorf("%s cpu pct greater than 100: %+v", name, cpuPct)
|
|
||||||
}
|
|
||||||
stats.Cpu = [2]uint64{statsJson.CPUStats.CPUUsage.TotalUsage, statsJson.CPUStats.SystemUsage}
|
|
||||||
|
|
||||||
// network
|
|
||||||
var total_sent, total_recv uint64
|
|
||||||
for _, v := range statsJson.Networks {
|
|
||||||
total_sent += v.TxBytes
|
|
||||||
total_recv += v.RxBytes
|
|
||||||
}
|
|
||||||
var sent_delta, recv_delta float64
|
|
||||||
// prevent first run from sending all prev sent/recv bytes
|
|
||||||
if initialized {
|
|
||||||
secondsElapsed := time.Since(stats.Net.Time).Seconds()
|
|
||||||
sent_delta = float64(total_sent-stats.Net.Sent) / secondsElapsed
|
|
||||||
recv_delta = float64(total_recv-stats.Net.Recv) / secondsElapsed
|
|
||||||
// log.Printf("sent delta: %+v, recv delta: %+v\n", sent_delta, recv_delta)
|
|
||||||
}
|
|
||||||
stats.Net.Sent = total_sent
|
|
||||||
stats.Net.Recv = total_recv
|
|
||||||
stats.Net.Time = time.Now()
|
|
||||||
|
|
||||||
cStats := &ContainerStats{
|
|
||||||
Name: name,
|
|
||||||
Cpu: twoDecimals(cpuPct),
|
|
||||||
Mem: bytesToMegabytes(float64(usedMemory)),
|
|
||||||
NetworkSent: bytesToMegabytes(sent_delta),
|
|
||||||
NetworkRecv: bytesToMegabytes(recv_delta),
|
|
||||||
}
|
|
||||||
return cStats, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// delete container stats from map using mutex
|
|
||||||
func deleteContainerStatsSync(id string) {
|
|
||||||
containerStatsMutex.Lock()
|
|
||||||
defer containerStatsMutex.Unlock()
|
|
||||||
delete(containerStatsMap, id)
|
|
||||||
}
|
|
||||||
|
|
||||||
func gatherStats() *SystemData {
|
|
||||||
systemInfo, systemStats := getSystemStats()
|
|
||||||
stats := &SystemData{
|
|
||||||
Stats: systemStats,
|
|
||||||
Info: systemInfo,
|
|
||||||
Containers: []*ContainerStats{},
|
|
||||||
}
|
|
||||||
containerStats, err := getDockerStats()
|
|
||||||
if err == nil {
|
|
||||||
stats.Containers = containerStats
|
|
||||||
}
|
|
||||||
// fmt.Printf("%+v\n", stats)
|
|
||||||
return stats
|
|
||||||
}
|
|
||||||
|
|
||||||
func startServer(addr string, pubKey []byte) {
|
|
||||||
sshServer.Handle(func(s sshServer.Session) {
|
|
||||||
stats := gatherStats()
|
|
||||||
var jsonStats []byte
|
|
||||||
jsonStats, _ = json.Marshal(stats)
|
|
||||||
io.WriteString(s, string(jsonStats))
|
|
||||||
s.Exit(0)
|
|
||||||
})
|
|
||||||
|
|
||||||
log.Printf("Starting SSH server on %s", addr)
|
|
||||||
if err := sshServer.ListenAndServe(addr, nil, sshServer.NoPty(),
|
|
||||||
sshServer.PublicKeyAuth(func(ctx sshServer.Context, key sshServer.PublicKey) bool {
|
|
||||||
data := []byte(pubKey)
|
|
||||||
allowed, _, _, _, _ := sshServer.ParseAuthorizedKey(data)
|
|
||||||
return sshServer.KeysEqual(key, allowed)
|
|
||||||
}),
|
|
||||||
); err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
// handle flags / subcommands
|
|
||||||
if len(os.Args) > 1 {
|
|
||||||
switch os.Args[1] {
|
|
||||||
case "-v":
|
|
||||||
fmt.Println("beszel-agent", Version)
|
|
||||||
case "update":
|
|
||||||
updateBeszel()
|
|
||||||
}
|
|
||||||
os.Exit(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
var pubKey []byte
|
|
||||||
if pubKeyEnv, exists := os.LookupEnv("KEY"); exists {
|
|
||||||
pubKey = []byte(pubKeyEnv)
|
|
||||||
} else {
|
|
||||||
log.Fatal("KEY environment variable is not set")
|
|
||||||
}
|
|
||||||
|
|
||||||
if filesystem, exists := os.LookupEnv("FILESYSTEM"); exists {
|
|
||||||
diskIoStats.Filesystem = filesystem
|
|
||||||
} else {
|
|
||||||
diskIoStats.Filesystem = findDefaultFilesystem()
|
|
||||||
}
|
|
||||||
|
|
||||||
initializeDiskIoStats()
|
|
||||||
initializeNetIoStats()
|
|
||||||
|
|
||||||
if port, exists := os.LookupEnv("PORT"); exists {
|
|
||||||
// allow passing an address in the form of "127.0.0.1:45876"
|
|
||||||
if !strings.Contains(port, ":") {
|
|
||||||
port = ":" + port
|
|
||||||
}
|
|
||||||
startServer(port, pubKey)
|
|
||||||
} else {
|
|
||||||
startServer(":45876", pubKey)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func bytesToMegabytes(b float64) float64 {
|
|
||||||
return twoDecimals(b / 1048576)
|
|
||||||
}
|
|
||||||
|
|
||||||
func bytesToGigabytes(b uint64) float64 {
|
|
||||||
return twoDecimals(float64(b) / 1073741824)
|
|
||||||
}
|
|
||||||
|
|
||||||
func twoDecimals(value float64) float64 {
|
|
||||||
return math.Round(value*100) / 100
|
|
||||||
}
|
|
||||||
|
|
||||||
func findDefaultFilesystem() string {
|
|
||||||
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 {
|
|
||||||
case strings.HasPrefix(v.Name, "lo"),
|
|
||||||
strings.HasPrefix(v.Name, "docker"),
|
|
||||||
strings.HasPrefix(v.Name, "br-"),
|
|
||||||
strings.HasPrefix(v.Name, "veth"),
|
|
||||||
v.BytesRecv == 0,
|
|
||||||
v.BytesSent == 0:
|
|
||||||
return true
|
|
||||||
default:
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func initializeDiskIoStats() {
|
|
||||||
if io, err := disk.IOCounters(diskIoStats.Filesystem); err == nil {
|
|
||||||
for _, d := range io {
|
|
||||||
diskIoStats.Time = time.Now()
|
|
||||||
diskIoStats.Read = d.ReadBytes
|
|
||||||
diskIoStats.Write = d.WriteBytes
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func initializeNetIoStats() {
|
|
||||||
if netIO, err := psutilNet.IOCounters(true); err == nil {
|
|
||||||
bytesSent := uint64(0)
|
|
||||||
bytesRecv := uint64(0)
|
|
||||||
for _, v := range netIO {
|
|
||||||
if skipNetworkInterface(&v) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
log.Printf("Found network interface: %+v (%+v recv, %+v sent)\n", v.Name, v.BytesRecv, v.BytesSent)
|
|
||||||
bytesSent += v.BytesSent
|
|
||||||
bytesRecv += v.BytesRecv
|
|
||||||
}
|
|
||||||
netIoStats.BytesSent = bytesSent
|
|
||||||
netIoStats.BytesRecv = bytesRecv
|
|
||||||
netIoStats.Time = time.Now()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func newDockerClient() *http.Client {
|
|
||||||
dockerHost := "unix:///var/run/docker.sock"
|
|
||||||
if dockerHostEnv, exists := os.LookupEnv("DOCKER_HOST"); exists {
|
|
||||||
dockerHost = dockerHostEnv
|
|
||||||
}
|
|
||||||
|
|
||||||
parsedURL, err := url.Parse(dockerHost)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal("Error parsing DOCKER_HOST: " + err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
transport := &http.Transport{
|
|
||||||
ForceAttemptHTTP2: false,
|
|
||||||
IdleConnTimeout: 90 * time.Second,
|
|
||||||
DisableCompression: true,
|
|
||||||
MaxIdleConnsPerHost: 20,
|
|
||||||
DisableKeepAlives: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
switch parsedURL.Scheme {
|
|
||||||
case "unix":
|
|
||||||
transport.DialContext = func(ctx context.Context, proto, addr string) (net.Conn, error) {
|
|
||||||
return (&net.Dialer{}).DialContext(ctx, "unix", parsedURL.Path)
|
|
||||||
}
|
|
||||||
case "tcp", "http", "https":
|
|
||||||
log.Println("Using DOCKER_HOST: " + dockerHost)
|
|
||||||
transport.DialContext = func(ctx context.Context, proto, addr string) (net.Conn, error) {
|
|
||||||
return (&net.Dialer{}).DialContext(ctx, "tcp", parsedURL.Host)
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
log.Fatal("Unsupported DOCKER_HOST: " + parsedURL.Scheme)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &http.Client{
|
|
||||||
Timeout: time.Second,
|
|
||||||
Transport: transport,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func closeIdleConnections(err error) {
|
|
||||||
log.Printf("Closing idle connections. Error: %+v\n", err)
|
|
||||||
dockerClient.Transport.(*http.Transport).CloseIdleConnections()
|
|
||||||
}
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/blang/semver"
|
|
||||||
"github.com/rhysd/go-github-selfupdate/selfupdate"
|
|
||||||
)
|
|
||||||
|
|
||||||
func updateBeszel() {
|
|
||||||
var latest *selfupdate.Release
|
|
||||||
var found bool
|
|
||||||
var err error
|
|
||||||
currentVersion := semver.MustParse(Version)
|
|
||||||
fmt.Println("beszel-agent", currentVersion)
|
|
||||||
fmt.Println("Checking for updates...")
|
|
||||||
updater, _ := selfupdate.NewUpdater(selfupdate.Config{
|
|
||||||
Filters: []string{"beszel-agent"},
|
|
||||||
})
|
|
||||||
latest, found, err = updater.DetectLatest("henrygd/beszel")
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println("Error checking for updates:", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !found {
|
|
||||||
fmt.Println("No updates found")
|
|
||||||
os.Exit(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Println("Latest version:", latest.Version)
|
|
||||||
|
|
||||||
if latest.Version.LTE(currentVersion) {
|
|
||||||
fmt.Println("You are up to date")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var binaryPath string
|
|
||||||
fmt.Printf("Updating from %s to %s...\n", currentVersion, latest.Version)
|
|
||||||
binaryPath, err = os.Executable()
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println("Error getting binary path:", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
err = selfupdate.UpdateTo(latest.AssetURL, binaryPath)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println("Please try rerunning with sudo. Error:", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
fmt.Printf("Successfully updated to %s\n\n%s\n", latest.Version, strings.TrimSpace(latest.ReleaseNotes))
|
|
||||||
}
|
|
||||||
69
beszel/.goreleaser.yml
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
version: 2
|
||||||
|
|
||||||
|
project_name: beszel
|
||||||
|
|
||||||
|
before:
|
||||||
|
hooks:
|
||||||
|
- go mod tidy
|
||||||
|
|
||||||
|
builds:
|
||||||
|
- id: beszel
|
||||||
|
binary: beszel
|
||||||
|
main: cmd/hub/hub.go
|
||||||
|
env:
|
||||||
|
- CGO_ENABLED=0
|
||||||
|
goos:
|
||||||
|
- linux
|
||||||
|
- darwin
|
||||||
|
goarch:
|
||||||
|
- amd64
|
||||||
|
- arm64
|
||||||
|
- arm
|
||||||
|
|
||||||
|
- id: beszel-agent
|
||||||
|
binary: beszel-agent
|
||||||
|
main: cmd/agent/agent.go
|
||||||
|
env:
|
||||||
|
- CGO_ENABLED=0
|
||||||
|
goos:
|
||||||
|
- linux
|
||||||
|
- darwin
|
||||||
|
- freebsd
|
||||||
|
goarch:
|
||||||
|
- amd64
|
||||||
|
- arm64
|
||||||
|
- arm
|
||||||
|
- mips64
|
||||||
|
ignore:
|
||||||
|
- goos: freebsd
|
||||||
|
goarch: arm
|
||||||
|
|
||||||
|
archives:
|
||||||
|
- id: beszel
|
||||||
|
format: tar.gz
|
||||||
|
builds:
|
||||||
|
- beszel-agent
|
||||||
|
name_template: >-
|
||||||
|
{{ .Binary }}_
|
||||||
|
{{- .Os }}_
|
||||||
|
{{- .Arch }}
|
||||||
|
- id: beszel-agent
|
||||||
|
format: tar.gz
|
||||||
|
builds:
|
||||||
|
- beszel
|
||||||
|
name_template: >-
|
||||||
|
{{ .Binary }}_
|
||||||
|
{{- .Os }}_
|
||||||
|
{{- .Arch }}
|
||||||
|
# use zip for windows archives
|
||||||
|
# format_overrides:
|
||||||
|
# - goos: windows
|
||||||
|
# format: zip
|
||||||
|
|
||||||
|
changelog:
|
||||||
|
disable: true
|
||||||
|
sort: asc
|
||||||
|
filters:
|
||||||
|
exclude:
|
||||||
|
- '^docs:'
|
||||||
|
- '^test:'
|
||||||
42
beszel/cmd/agent/agent.go
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"beszel"
|
||||||
|
"beszel/internal/agent"
|
||||||
|
"beszel/internal/update"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// handle flags / subcommands
|
||||||
|
if len(os.Args) > 1 {
|
||||||
|
switch os.Args[1] {
|
||||||
|
case "-v":
|
||||||
|
fmt.Println(beszel.AppName+"-agent", beszel.Version)
|
||||||
|
case "update":
|
||||||
|
update.UpdateBeszelAgent()
|
||||||
|
}
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
var pubKey []byte
|
||||||
|
if pubKeyEnv, exists := os.LookupEnv("KEY"); exists {
|
||||||
|
pubKey = []byte(pubKeyEnv)
|
||||||
|
} else {
|
||||||
|
log.Fatal("KEY environment variable is not set")
|
||||||
|
}
|
||||||
|
|
||||||
|
addr := ":45876"
|
||||||
|
if portEnvVar, exists := os.LookupEnv("PORT"); exists {
|
||||||
|
// allow passing an address in the form of "127.0.0.1:45876"
|
||||||
|
if !strings.Contains(portEnvVar, ":") {
|
||||||
|
portEnvVar = ":" + portEnvVar
|
||||||
|
}
|
||||||
|
addr = portEnvVar
|
||||||
|
}
|
||||||
|
|
||||||
|
agent.NewAgent(pubKey, addr).Run()
|
||||||
|
}
|
||||||
29
beszel/cmd/hub/hub.go
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"beszel"
|
||||||
|
"beszel/internal/hub"
|
||||||
|
"beszel/internal/update"
|
||||||
|
_ "beszel/migrations"
|
||||||
|
|
||||||
|
"github.com/pocketbase/pocketbase"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
app := pocketbase.NewWithConfig(pocketbase.Config{
|
||||||
|
DefaultDataDir: beszel.AppName + "_data",
|
||||||
|
})
|
||||||
|
app.RootCmd.Version = beszel.Version
|
||||||
|
app.RootCmd.Use = beszel.AppName
|
||||||
|
app.RootCmd.Short = ""
|
||||||
|
|
||||||
|
// add update command
|
||||||
|
app.RootCmd.AddCommand(&cobra.Command{
|
||||||
|
Use: "update",
|
||||||
|
Short: "Update " + beszel.AppName + " to the latest version",
|
||||||
|
Run: func(_ *cobra.Command, _ []string) { update.UpdateBeszel() },
|
||||||
|
})
|
||||||
|
|
||||||
|
hub.NewHub(app).Run()
|
||||||
|
}
|
||||||
@@ -2,15 +2,15 @@ FROM --platform=$BUILDPLATFORM golang:alpine AS builder
|
|||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Download Go modules
|
|
||||||
COPY go.mod go.sum ./
|
COPY go.mod go.sum ./
|
||||||
RUN go mod download
|
# RUN go mod download
|
||||||
|
|
||||||
COPY *.go ./
|
COPY *.go ./
|
||||||
|
COPY cmd ./cmd
|
||||||
|
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 .
|
RUN CGO_ENABLED=0 GOGC=75 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -ldflags "-w -s" -o /agent ./cmd/agent
|
||||||
|
|
||||||
# ? -------------------------
|
# ? -------------------------
|
||||||
FROM scratch
|
FROM scratch
|
||||||
@@ -8,6 +8,8 @@ RUN go mod download
|
|||||||
|
|
||||||
# Copy source files
|
# Copy source files
|
||||||
COPY *.go ./
|
COPY *.go ./
|
||||||
|
COPY cmd ./cmd
|
||||||
|
COPY internal ./internal
|
||||||
COPY migrations ./migrations
|
COPY migrations ./migrations
|
||||||
COPY site/dist ./site/dist
|
COPY site/dist ./site/dist
|
||||||
COPY site/*.go ./site
|
COPY site/*.go ./site
|
||||||
@@ -20,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 .
|
RUN CGO_ENABLED=0 GOGC=75 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -ldflags "-w -s" -o /beszel ./cmd/hub
|
||||||
|
|
||||||
# ? -------------------------
|
# ? -------------------------
|
||||||
FROM scratch
|
FROM scratch
|
||||||
99
beszel/go.mod
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
module beszel
|
||||||
|
|
||||||
|
go 1.22.4
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/blang/semver v3.5.1+incompatible
|
||||||
|
github.com/gliderlabs/ssh v0.3.7
|
||||||
|
github.com/goccy/go-json v0.10.3
|
||||||
|
github.com/labstack/echo/v5 v5.0.0-20230722203903-ec5b858dab61
|
||||||
|
github.com/pocketbase/dbx v1.10.1
|
||||||
|
github.com/pocketbase/pocketbase v0.22.20
|
||||||
|
github.com/rhysd/go-github-selfupdate v1.2.3
|
||||||
|
github.com/shirou/gopsutil/v4 v4.24.8
|
||||||
|
github.com/spf13/cobra v1.8.1
|
||||||
|
golang.org/x/crypto v0.26.0
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/AlecAivazis/survey/v2 v2.3.7 // indirect
|
||||||
|
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect
|
||||||
|
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // 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/config v1.27.32 // 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.13 // 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.17 // 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/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/checksum v1.3.19 // 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.17 // 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.6 // 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.6 // indirect
|
||||||
|
github.com/aws/smithy-go v1.20.4 // indirect
|
||||||
|
github.com/disintegration/imaging v1.6.2 // indirect
|
||||||
|
github.com/domodwyer/mailyak/v3 v3.6.2 // indirect
|
||||||
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
|
github.com/fatih/color v1.17.0 // indirect
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.5 // indirect
|
||||||
|
github.com/ganigeorgiev/fexpr v0.4.1 // indirect
|
||||||
|
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||||
|
github.com/go-ozzo/ozzo-validation/v4 v4.3.0 // indirect
|
||||||
|
github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
|
||||||
|
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||||
|
github.com/google/go-github/v30 v30.1.0 // indirect
|
||||||
|
github.com/google/go-querystring v1.1.0 // indirect
|
||||||
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
|
github.com/googleapis/gax-go/v2 v2.13.0 // indirect
|
||||||
|
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
|
||||||
|
github.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf // indirect
|
||||||
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
|
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
|
||||||
|
github.com/lufia/plan9stats v0.0.0-20240819163618-b1d8f4d146e7 // indirect
|
||||||
|
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.22 // indirect
|
||||||
|
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
|
||||||
|
github.com/ncruces/go-strftime v0.1.9 // indirect
|
||||||
|
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // 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/pflag v1.0.5 // indirect
|
||||||
|
github.com/tcnksm/go-gitconfig v0.1.2 // indirect
|
||||||
|
github.com/tklauser/go-sysconf v0.3.14 // indirect
|
||||||
|
github.com/tklauser/numcpus v0.8.0 // indirect
|
||||||
|
github.com/ulikunitz/xz v0.5.12 // indirect
|
||||||
|
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||||
|
github.com/valyala/fasttemplate v1.2.2 // indirect
|
||||||
|
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||||
|
go.opencensus.io v0.24.0 // indirect
|
||||||
|
gocloud.dev v0.39.0 // indirect
|
||||||
|
golang.org/x/image v0.19.0 // indirect
|
||||||
|
golang.org/x/net v0.28.0 // indirect
|
||||||
|
golang.org/x/oauth2 v0.22.0 // indirect
|
||||||
|
golang.org/x/sync v0.8.0 // indirect
|
||||||
|
golang.org/x/sys v0.24.0 // indirect
|
||||||
|
golang.org/x/term v0.23.0 // indirect
|
||||||
|
golang.org/x/text v0.17.0 // indirect
|
||||||
|
golang.org/x/time v0.6.0 // indirect
|
||||||
|
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect
|
||||||
|
google.golang.org/api v0.196.0 // indirect
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 // indirect
|
||||||
|
google.golang.org/grpc v1.66.0 // indirect
|
||||||
|
google.golang.org/protobuf v1.34.2 // indirect
|
||||||
|
modernc.org/gc/v3 v3.0.0-20240801135723-a856999a2e4a // indirect
|
||||||
|
modernc.org/libc v1.60.1 // indirect
|
||||||
|
modernc.org/mathutil v1.6.0 // indirect
|
||||||
|
modernc.org/memory v1.8.0 // indirect
|
||||||
|
modernc.org/sqlite v1.32.0 // indirect
|
||||||
|
modernc.org/strutil v1.2.0 // indirect
|
||||||
|
modernc.org/token v1.1.0 // indirect
|
||||||
|
)
|
||||||
@@ -1,16 +1,16 @@
|
|||||||
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.0 h1:CnFSK6Xo3lDYRoBKEcAtia6VSC837/ZkJuRduSFnr14=
|
cloud.google.com/go v0.115.1 h1:Jo0SM9cQnSkYfp44+v+NQXHpcHqlnRJk2qxh6yvxxxQ=
|
||||||
cloud.google.com/go v0.115.0/go.mod h1:8jIM5vVgoAEoiVxQ/O4BFTfHqulPZgs/ufEzMcFMdWU=
|
cloud.google.com/go v0.115.1/go.mod h1:DuujITeaufu3gL68/lOFIirVNJwQeyf5UXyi+Wbgknc=
|
||||||
cloud.google.com/go/auth v0.7.2 h1:uiha352VrCDMXg+yoBtaD0tUF4Kv9vrtrWPYXwutnDE=
|
cloud.google.com/go/auth v0.9.3 h1:VOEUIAADkkLtyfr3BLa3R8Ed/j6w1jTBmARx+wb5w5U=
|
||||||
cloud.google.com/go/auth v0.7.2/go.mod h1:VEc4p5NNxycWQTMQEDQF0bd6aTMb6VgYDXEwiJJQAbs=
|
cloud.google.com/go/auth v0.9.3/go.mod h1:7z6VY+7h3KUdRov5F1i8NDP5ZzWKYmEPO842BgCsmTk=
|
||||||
cloud.google.com/go/auth/oauth2adapt v0.2.3 h1:MlxF+Pd3OmSudg/b1yZ5lJwoXCEaeedAguodky1PcKI=
|
cloud.google.com/go/auth/oauth2adapt v0.2.4 h1:0GWE/FUsXhf6C+jAkWgYm7X9tK8cuEIfy19DBn6B6bY=
|
||||||
cloud.google.com/go/auth/oauth2adapt v0.2.3/go.mod h1:tMQXOfZzFuNuUxOypHlQEXgdfX5cuhwU+ffUuXRJE8I=
|
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=
|
||||||
cloud.google.com/go/compute/metadata v0.5.0/go.mod h1:aHnloV2TPI38yx4s9+wAZhHykWvVCfu7hQbF+9CWoiY=
|
cloud.google.com/go/compute/metadata v0.5.0/go.mod h1:aHnloV2TPI38yx4s9+wAZhHykWvVCfu7hQbF+9CWoiY=
|
||||||
cloud.google.com/go/iam v1.1.7 h1:z4VHOhwKLF/+UYXAJDFwGtNF0b6gjsW1Pk9Ml0U/IoM=
|
cloud.google.com/go/iam v1.1.13 h1:7zWBXG9ERbMLrzQBRhFliAV+kjcRToDTgQT3CTwYyv4=
|
||||||
cloud.google.com/go/iam v1.1.7/go.mod h1:J4PMPg8TtyurAUvSmPj8FF3EDgY1SPRZxcUGrn7WXGA=
|
cloud.google.com/go/iam v1.1.13/go.mod h1:K8mY0uSXwEXS30KrnVb+j54LB/ntfZu1dr+4zFMNbus=
|
||||||
cloud.google.com/go/storage v1.40.0 h1:VEpDQV5CJxFmJ6ueWNsKxcr1QAYOXEgxDa+sBbJahPw=
|
cloud.google.com/go/storage v1.43.0 h1:CcxnSohZwizt4LCzQHWvBf1/kvtHUn7gk9QERXPyXFs=
|
||||||
cloud.google.com/go/storage v1.40.0/go.mod h1:Rrj7/hKlG87BLqDJYtwR0fbPld8uJPbQ2ucUMY7Ir0g=
|
cloud.google.com/go/storage v1.43.0/go.mod h1:ajvxEa7WmZS1PxvKRq4bq0tFT3vMd502JwstCcYv0Q0=
|
||||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||||
github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ=
|
github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ=
|
||||||
@@ -18,49 +18,51 @@ github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1r
|
|||||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||||
github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s=
|
github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s=
|
||||||
github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w=
|
github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w=
|
||||||
|
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
|
||||||
|
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
|
||||||
github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg=
|
github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg=
|
||||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=
|
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=
|
||||||
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.51.30 h1:RVFkjn9P0JMwnuZCVH0TlV5k9zepHzlbc4943eZMhGw=
|
github.com/aws/aws-sdk-go v1.55.5 h1:KKUZBfBoyqy5d3swXyiC7Q76ic40rYcbqH7qjh59kzU=
|
||||||
github.com/aws/aws-sdk-go v1.51.30/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk=
|
github.com/aws/aws-sdk-go v1.55.5/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU=
|
||||||
github.com/aws/aws-sdk-go-v2 v1.30.3 h1:jUeBtG0Ih+ZIFH0F4UkmL9w3cSpaMv9tYYDbzILP8dY=
|
github.com/aws/aws-sdk-go-v2 v1.30.5 h1:mWSRTwQAb0aLE17dSzztCVJWI9+cRMgqebndjwDyK0g=
|
||||||
github.com/aws/aws-sdk-go-v2 v1.30.3/go.mod h1:nIQjQVp5sfpQcTc9mPSr1B0PaWK5ByX9MOoDadSN4lc=
|
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.3 h1:tW1/Rkad38LA15X4UQtjXZXNKsCgkshC3EbmcUmghTg=
|
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.3/go.mod h1:UbnqO+zjqk3uIt9yCACHJ9IVNhyhOCnYk8yA19SAWrM=
|
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.27 h1:HdqgGt1OAP0HkEDDShEl0oSYa9ZZBSOmKpdpsDMdO90=
|
github.com/aws/aws-sdk-go-v2/config v1.27.32 h1:jnAMVTJTpAQlePCUUlnXnllHEMGVWmvUJOiGjgtS9S0=
|
||||||
github.com/aws/aws-sdk-go-v2/config v1.27.27/go.mod h1:MVYamCg76dFNINkZFu4n4RjDixhVr51HLj4ErWzrVwg=
|
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.27 h1:2raNba6gr2IfA0eqqiP2XiQ0UVOpGPgDSi0I9iAP+UI=
|
github.com/aws/aws-sdk-go-v2/credentials v1.17.31 h1:jtyfcOfgoqWA2hW/E8sFbwdfgwD3APnF9CLCKE8dTyw=
|
||||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.27/go.mod h1:gniiwbGahQByxan6YjQUMcW4Aov6bLC3m+evgcoN4r4=
|
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.11 h1:KreluoV8FZDEtI6Co2xuNk/UqI9iwMrOx/87PBNIKqw=
|
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.11/go.mod h1:SeSUYBLsMYFoRvHE0Tjvn7kbxaUhl75CJi1sbfhMxkU=
|
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.9 h1:TC2vjvaAv1VNl9A0rm+SeuBjrzXnrlwk6Yop+gKRi38=
|
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.9/go.mod h1:WPv2FRnkIOoDv/8j2gSUsI4qDc7392w5anFB/I89GZ8=
|
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.15 h1:SoNJ4RlFEQEbtDcCEt+QG56MY4fm4W8rYirAmq+/DdU=
|
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.15/go.mod h1:U9ke74k1n2bf+RIgoX1SXFed1HLs51OgUSs+Ph0KJP8=
|
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.15 h1:C6WHdGnTDIYETAm5iErQUiVNsclNx9qbJVPIt03B6bI=
|
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.15/go.mod h1:ZQLZqhcu+JhSrA9/NXRm8SkDvsycE+JkV3WGY41e+IM=
|
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.0 h1:hT8rVHwugYE2lEfdFE0QWVo81lF7jMrYJVDWI+f+VxU=
|
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.0/go.mod h1:8tu/lYfQfFe6IGnaOdrpVgEL2IrrDOf6/m9RQum4NkY=
|
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.15 h1:Z5r7SycxmSllHYmaAZPpmN8GviDrSGhMS6bldqtXZPw=
|
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.15/go.mod h1:CetW7bDE00QoGEmPUoZuRog07SGVAUVW6LFpNP0YfIg=
|
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.3 h1:dT3MqvGhSoaIhRseqw2I0yH81l7wiR2vjs57O51EAm8=
|
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.3/go.mod h1:GlAeCkHwugxdHaueRr4nhPuY+WW+gR8UjlcqzPr1SPI=
|
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.17 h1:YPYe6ZmvUfDDDELqEKtAd6bo8zxhkm+XEFEzQisqUIE=
|
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.17/go.mod h1:oBtcnYua/CgzCWYN7NZ5j7PotFDaFSUjCYVTtfyn7vw=
|
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.17 h1:HGErhhrxZlQ044RiM+WdoZxp0p+EGM62y3L6pwA4olE=
|
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.17/go.mod h1:RkZEx4l0EHYDJpWppMJ3nD9wZJAa8/0lq9aVC+r2UII=
|
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.15 h1:246A4lSTXWJw/rmlQI+TT2OcqeDMKBdyjEQrafMaQdA=
|
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.15/go.mod h1:haVfg3761/WF7YPuJOER2MP0k4UAXyHaLclKXB6usDg=
|
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.58.2 h1:sZXIzO38GZOU+O0C+INqbH7C2yALwfMWpd64tONS/NE=
|
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.58.2/go.mod h1:Lcxzg5rojyVPU/0eFwLtcyTaek/6Mtic5B1gJo7e/zE=
|
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.4 h1:BXx0ZIxvrJdSgSvKTZ+yRBeSqqgPM89VPlulEcl37tM=
|
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.4/go.mod h1:ooyCOXjvJEsUw7x+ZDHeISPMhtwI3ZCB7ggFMcFfWLU=
|
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.4 h1:yiwVzJW2ZxZTurVbYWA7QOrAaCYQR72t0wrSBfoesUE=
|
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.4/go.mod h1:0oxfLkpz3rQ/CHlx5hB7H69YUpFiI1tql6Q6Ne+1bCw=
|
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.3 h1:ZsDKRLXGWHk8WdtyYMoGNO7bTudrvuKpDKgMVRlepGE=
|
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.3/go.mod h1:zwySh8fpFyXp9yOr/KVzxOl8SRqgf/IDw5aUt9UKFcQ=
|
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.3 h1:ryHwveWzPV5BIof6fyDvor6V3iUL7nTfiTKXHiW05nE=
|
github.com/aws/smithy-go v1.20.4 h1:2HK1zBdPgRbjFOHlfeQZfpC4r72MOb9bZkiFwggKO+4=
|
||||||
github.com/aws/smithy-go v1.20.3/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E=
|
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=
|
||||||
github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
|
github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
|
||||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||||
@@ -93,10 +95,15 @@ github.com/gabriel-vasile/mimetype v1.4.5 h1:J7wGKdGu33ocBOhGy0z653k/lFKLFDPJMG8
|
|||||||
github.com/gabriel-vasile/mimetype v1.4.5/go.mod h1:ibHel+/kbxn9x2407k1izTA1S81ku1z/DlgOW2QE0M4=
|
github.com/gabriel-vasile/mimetype v1.4.5/go.mod h1:ibHel+/kbxn9x2407k1izTA1S81ku1z/DlgOW2QE0M4=
|
||||||
github.com/ganigeorgiev/fexpr v0.4.1 h1:hpUgbUEEWIZhSDBtf4M9aUNfQQ0BZkGRaMePy7Gcx5k=
|
github.com/ganigeorgiev/fexpr v0.4.1 h1:hpUgbUEEWIZhSDBtf4M9aUNfQQ0BZkGRaMePy7Gcx5k=
|
||||||
github.com/ganigeorgiev/fexpr v0.4.1/go.mod h1:RyGiGqmeXhEQ6+mlGdnUleLHgtzzu/VGO2WtJkF5drE=
|
github.com/ganigeorgiev/fexpr v0.4.1/go.mod h1:RyGiGqmeXhEQ6+mlGdnUleLHgtzzu/VGO2WtJkF5drE=
|
||||||
|
github.com/gliderlabs/ssh v0.3.7 h1:iV3Bqi942d9huXnzEF2Mt+CY9gLu8DNM4Obd+8bODRE=
|
||||||
|
github.com/gliderlabs/ssh v0.3.7/go.mod h1:zpHEXBstFnQYtGnB8k8kQLol82umzn/2/snG7alWVD8=
|
||||||
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||||
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||||
|
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||||
|
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
||||||
|
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
||||||
github.com/go-ozzo/ozzo-validation/v4 v4.3.0 h1:byhDUpfEwjsVQb1vBunvIjh2BHQ9ead57VkAEY4V+Es=
|
github.com/go-ozzo/ozzo-validation/v4 v4.3.0 h1:byhDUpfEwjsVQb1vBunvIjh2BHQ9ead57VkAEY4V+Es=
|
||||||
github.com/go-ozzo/ozzo-validation/v4 v4.3.0/go.mod h1:2NKgrcHl3z6cJs+3Oo940FPRiTzuqKbvfrL2RxCj6Ew=
|
github.com/go-ozzo/ozzo-validation/v4 v4.3.0/go.mod h1:2NKgrcHl3z6cJs+3Oo940FPRiTzuqKbvfrL2RxCj6Ew=
|
||||||
github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
|
github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
|
||||||
@@ -137,17 +144,17 @@ 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.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o=
|
github.com/google/s2a-go v0.1.8 h1:zZDs9gcbt9ZPLV0ndSyQk6Kacx2g/X+SKYovpnz3SMM=
|
||||||
github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw=
|
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=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
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=
|
||||||
@@ -172,6 +179,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
|||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
github.com/labstack/echo/v5 v5.0.0-20230722203903-ec5b858dab61 h1:FwuzbVh87iLiUQj1+uQUsuw9x5t9m5n5g7rG7o4svW4=
|
github.com/labstack/echo/v5 v5.0.0-20230722203903-ec5b858dab61 h1:FwuzbVh87iLiUQj1+uQUsuw9x5t9m5n5g7rG7o4svW4=
|
||||||
github.com/labstack/echo/v5 v5.0.0-20230722203903-ec5b858dab61/go.mod h1:paQfF1YtHe+GrGg5fOgjsjoCX/UKDr9bc1DoWpZfns8=
|
github.com/labstack/echo/v5 v5.0.0-20230722203903-ec5b858dab61/go.mod h1:paQfF1YtHe+GrGg5fOgjsjoCX/UKDr9bc1DoWpZfns8=
|
||||||
|
github.com/lufia/plan9stats v0.0.0-20240819163618-b1d8f4d146e7 h1:5RK988zAqB3/AN3opGfRpoQgAVqr6/A5+qRTi67VUZY=
|
||||||
|
github.com/lufia/plan9stats v0.0.0-20240819163618-b1d8f4d146e7/go.mod h1:ilwx/Dta8jXAgpFYFvSWEMwxmbWXyiUHkd5FwyKhb5k=
|
||||||
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||||
@@ -193,8 +202,10 @@ 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.18 h1:yVckUhi5GDORqCb0BbtlvRB1CVxHY9HO9btEaeZHVJU=
|
github.com/pocketbase/pocketbase v0.22.20 h1:yUkhO5bTPWlzD4ZK6EQlS4R3AcHKDlBD+DxxU2BR83I=
|
||||||
github.com/pocketbase/pocketbase v0.22.18/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/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=
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||||
@@ -203,8 +214,14 @@ 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/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
|
github.com/shirou/gopsutil/v4 v4.24.8 h1:pVQjIenQkIhqO81mwTaXjTzOMT7d3TZkf43PlVFHENI=
|
||||||
github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
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/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/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=
|
||||||
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
|
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
|
||||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||||
@@ -217,10 +234,14 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
|
|||||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||||
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
|
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||||
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
github.com/tcnksm/go-gitconfig v0.1.2 h1:iiDhRitByXAEyjgBqsKi9QU4o2TNtv9kPP3RgPgXBPw=
|
github.com/tcnksm/go-gitconfig v0.1.2 h1:iiDhRitByXAEyjgBqsKi9QU4o2TNtv9kPP3RgPgXBPw=
|
||||||
github.com/tcnksm/go-gitconfig v0.1.2/go.mod h1:/8EhP4H7oJZdIPyT+/UIsG87kTzrzM4UsLGSItWYCpE=
|
github.com/tcnksm/go-gitconfig v0.1.2/go.mod h1:/8EhP4H7oJZdIPyT+/UIsG87kTzrzM4UsLGSItWYCpE=
|
||||||
|
github.com/tklauser/go-sysconf v0.3.14 h1:g5vzr9iPFFz24v2KZXs/pvpvh8/V9Fw6vQK5ZZb78yU=
|
||||||
|
github.com/tklauser/go-sysconf v0.3.14/go.mod h1:1ym4lWMLUOhuBOPGtRcJm7tEGX4SCYNEEEtghGG/8uY=
|
||||||
|
github.com/tklauser/numcpus v0.8.0 h1:Mx4Wwe/FjZLeQsK/6kt2EOepwwSl7SmJrK5bV/dXYgY=
|
||||||
|
github.com/tklauser/numcpus v0.8.0/go.mod h1:ZJZlAY+dmR4eut8epnzf0u/VwodKmryxR8txiloSqBE=
|
||||||
github.com/ulikunitz/xz v0.5.9/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
github.com/ulikunitz/xz v0.5.9/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||||
github.com/ulikunitz/xz v0.5.12 h1:37Nm15o69RwBkXM0J6A5OlE67RZTfzUxTj8fB3dfcsc=
|
github.com/ulikunitz/xz v0.5.12 h1:37Nm15o69RwBkXM0J6A5OlE67RZTfzUxTj8fB3dfcsc=
|
||||||
github.com/ulikunitz/xz v0.5.12/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
github.com/ulikunitz/xz v0.5.12/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||||
@@ -229,30 +250,32 @@ github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyC
|
|||||||
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
|
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
|
||||||
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
|
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
|
||||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
|
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
||||||
|
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.51.0 h1:A3SayB3rNyt+1S6qpI9mHPkeHTZbD7XILEqWnYZb2l0=
|
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.51.0/go.mod h1:27iA5uvhuRNmalO+iEUdVn5ZMj2qy10Mm+XRIpRmyuU=
|
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.51.0 h1:Xs2Ncz0gNihqu9iosIZ5SkBbWo5T8JhhLJFMQL1qmLI=
|
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.51.0/go.mod h1:vy+2G/6NvVMpwGX/NyLqcC41fxepnuKHk16E6IZUcJc=
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0/go.mod h1:L7UH0GbB0p47T4Rri3uHjbpCFYrVrwc1I25QhNPiGK8=
|
||||||
go.opentelemetry.io/otel v1.26.0 h1:LQwgL5s/1W7YiiRwxf03QGnWLb2HW4pLiAhaA5cZXBs=
|
go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw=
|
||||||
go.opentelemetry.io/otel v1.26.0/go.mod h1:UmLkJHUAidDval2EICqBMbnAd0/m2vmpf/dAM+fvFs4=
|
go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8=
|
||||||
go.opentelemetry.io/otel/metric v1.26.0 h1:7S39CLuY5Jgg9CrnA9HHiEjGMF/X2VHvoXGgSllRz30=
|
go.opentelemetry.io/otel/metric v1.29.0 h1:vPf/HFWTNkPu1aYeIsc98l4ktOQaL6LeSoeV2g+8YLc=
|
||||||
go.opentelemetry.io/otel/metric v1.26.0/go.mod h1:SY+rHOI4cEawI9a7N1A4nIg/nTQXe1ccCNWYOJUrpX4=
|
go.opentelemetry.io/otel/metric v1.29.0/go.mod h1:auu/QWieFVWx+DmQOUMgj0F8LHWdgalxXqvp7BII/W8=
|
||||||
go.opentelemetry.io/otel/trace v1.26.0 h1:1ieeAUb4y0TE26jUFrCIXKpTuVK7uJGN9/Z/2LP5sQA=
|
go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4=
|
||||||
go.opentelemetry.io/otel/trace v1.26.0/go.mod h1:4iDxvGDQuUkHve82hJJ8UqrwswHYsZuWCBllGV2U2y0=
|
go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ=
|
||||||
gocloud.dev v0.38.0 h1:SpxfaOc/Fp4PeO8ui7wRcCZV0EgXZ+IWcVSLn6ZMSw0=
|
gocloud.dev v0.39.0 h1:EYABYGhAalPUaMrbSKOr5lejxoxvXj99nE8XFtsDgds=
|
||||||
gocloud.dev v0.38.0/go.mod h1:3XjKvd2E5iVNu/xFImRzjN0d/fkNHe4s0RiKidpEUMQ=
|
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=
|
||||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
||||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
|
golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw=
|
||||||
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
|
golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
|
||||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||||
golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ=
|
golang.org/x/image v0.19.0 h1:D9FX4QWkLfkeqaC62SonffIIuYdOk/UE2XKUBgRIBIQ=
|
||||||
golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E=
|
golang.org/x/image v0.19.0/go.mod h1:y0zrRqlQRWQ5PXaYCOMLTW2fpsxZ8Qh9I/ohnInJEys=
|
||||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||||
@@ -270,48 +293,51 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL
|
|||||||
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
|
golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE=
|
||||||
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
|
golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg=
|
||||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||||
golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||||
golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs=
|
golang.org/x/oauth2 v0.22.0 h1:BzDx2FehcG7jJwgWLELCdmLuxk2i+x9UDpSiss2u0ZA=
|
||||||
golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
golang.org/x/oauth2 v0.22.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
|
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
|
||||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
|
golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg=
|
||||||
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk=
|
golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU=
|
||||||
golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4=
|
golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
|
golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc=
|
||||||
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
|
golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||||
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U=
|
||||||
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||||
@@ -323,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.189.0 h1:equMo30LypAkdkLMBqfeIqtyAnlyig1JSZArl4XPwdI=
|
google.golang.org/api v0.196.0 h1:k/RafYqebaIJBO3+SMnfEGtFVlvp5vSgqTUF54UN/zg=
|
||||||
google.golang.org/api v0.189.0/go.mod h1:FLWGJKb0hb+pU2j+rJqwbnsF+ym+fQs73rbJ+KAUgy8=
|
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=
|
||||||
@@ -334,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-20240722135656-d784300faade h1:lKFsS7wpngDgSCeFn7MoLy+wBDQZ1UQIJD4UNM1Qvkg=
|
google.golang.org/genproto v0.0.0-20240903143218-8af14fe29dc1 h1:BulPr26Jqjnd4eYDVe+YvyR7Yc2vJGkO5/0UxD0/jZU=
|
||||||
google.golang.org/genproto v0.0.0-20240722135656-d784300faade/go.mod h1:FfBgJBJg9GcpPvKIuHSZ/aE1g2ecGL74upMzGZjiGEY=
|
google.golang.org/genproto v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:hL97c3SYopEHblzpxRL4lSs523++l8DYxGM1FQiYmb4=
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20240711142825-46eb208f015d h1:kHjw/5UfflP/L5EbledDrcG4C2597RtymmGRZvHiCuY=
|
google.golang.org/genproto/googleapis/api v0.0.0-20240812133136-8ffd90a71988 h1:+/tmTy5zAieooKIXfzDm9KiA3Bv6JBwriRN9LY+yayk=
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20240711142825-46eb208f015d/go.mod h1:mw8MG/Qz5wfgYr6VqVCiZcHe/GJEfI+oGGDCohaVgB0=
|
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-20240725223205-93522f1f2a9f h1:RARaIm8pxYuxyNPbBQf5igT7XdOyCNtat1qAT2ZxjU4=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 h1:pPJltXNxVzT4pK9yD8vR9X75DaWYYmLGMsEvBfFQZzQ=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240725223205-93522f1f2a9f/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY=
|
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=
|
||||||
@@ -363,9 +389,8 @@ gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8
|
|||||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
||||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
|
|
||||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
@@ -373,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.5 h1:s04akhT2dysD0DFOlv9fkQ6oUTLPYgMnnDk9oaqjszM=
|
modernc.org/ccgo/v4 v4.21.0 h1:kKPI3dF7RIag8YcToh5ZwDcVMIv6VGa0ED5cvh0LMW4=
|
||||||
modernc.org/ccgo/v4 v4.20.5/go.mod h1:fYXClPUMWxWaz1Xj5sHbzW/ZENEFeuHLToqBxUk41nE=
|
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.4.3 h1:Ik4ZcMbC7aY4ZDPUhzXVXi7GMub9QcXLTfXn3mWpNw8=
|
modernc.org/gc/v2 v2.5.0 h1:bJ9ChznK1L1mUtAQtxi0wi5AtAs5jQuw4PrPHO5pb6M=
|
||||||
modernc.org/gc/v2 v2.4.3/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-20240722195230-4a140ff9c08e h1:WPC4v0rNIFb2PY+nBBEEKyugPPRHPzUgyN3xZPpGK58=
|
modernc.org/gc/v3 v3.0.0-20240801135723-a856999a2e4a h1:CfbpOLEo2IwNzJdMvE8aiRbPMxoTpgAJeyePh0SmO8M=
|
||||||
modernc.org/gc/v3 v3.0.0-20240722195230-4a140ff9c08e/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.55.5 h1:7duW01NsK0fIJ1xctdujeNDO46yPC89t0TZwDC5fE5k=
|
modernc.org/libc v1.60.1 h1:at373l8IFRTkJIkAU85BIuUoBM4T1b51ds0E1ovPG2s=
|
||||||
modernc.org/libc v1.55.5/go.mod h1:JXguUpMkbw1gknxspNE9XaG+kk9hDAAnBxpA6KGLiyA=
|
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=
|
||||||
@@ -391,8 +416,8 @@ modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
|
|||||||
modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
|
modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
|
||||||
modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc=
|
modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc=
|
||||||
modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss=
|
modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss=
|
||||||
modernc.org/sqlite v1.31.1 h1:XVU0VyzxrYHlBhIs1DiEgSl0ZtdnPtbLVy8hSkzxGrs=
|
modernc.org/sqlite v1.32.0 h1:6BM4uGza7bWypsw4fdLRsLxut6bHe4c58VeqjRgST8s=
|
||||||
modernc.org/sqlite v1.31.1/go.mod h1:UqoylwmTb9F+IqXERT8bW9zzOWN8qwAIcLdzeBZs4hA=
|
modernc.org/sqlite v1.32.0/go.mod h1:UqoylwmTb9F+IqXERT8bW9zzOWN8qwAIcLdzeBZs4hA=
|
||||||
modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA=
|
modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA=
|
||||||
modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0=
|
modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0=
|
||||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||||
661
beszel/internal/agent/agent.go
Normal file
@@ -0,0 +1,661 @@
|
|||||||
|
package agent
|
||||||
|
|
||||||
|
import (
|
||||||
|
"beszel"
|
||||||
|
"beszel/internal/entities/container"
|
||||||
|
"beszel/internal/entities/system"
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"math"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/shirou/gopsutil/v4/cpu"
|
||||||
|
"github.com/shirou/gopsutil/v4/disk"
|
||||||
|
"github.com/shirou/gopsutil/v4/host"
|
||||||
|
"github.com/shirou/gopsutil/v4/mem"
|
||||||
|
"github.com/shirou/gopsutil/v4/sensors"
|
||||||
|
|
||||||
|
sshServer "github.com/gliderlabs/ssh"
|
||||||
|
psutilNet "github.com/shirou/gopsutil/v4/net"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Agent struct {
|
||||||
|
addr string
|
||||||
|
pubKey []byte
|
||||||
|
sem chan struct{}
|
||||||
|
containerStatsMap map[string]*container.PrevContainerStats
|
||||||
|
containerStatsMutex *sync.Mutex
|
||||||
|
fsNames []string
|
||||||
|
fsStats map[string]*system.FsStats
|
||||||
|
netInterfaces map[string]struct{}
|
||||||
|
netIoStats *system.NetIoStats
|
||||||
|
dockerClient *http.Client
|
||||||
|
bufferPool *sync.Pool
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAgent(pubKey []byte, addr string) *Agent {
|
||||||
|
return &Agent{
|
||||||
|
addr: addr,
|
||||||
|
pubKey: pubKey,
|
||||||
|
sem: make(chan struct{}, 15),
|
||||||
|
containerStatsMap: make(map[string]*container.PrevContainerStats),
|
||||||
|
containerStatsMutex: &sync.Mutex{},
|
||||||
|
netIoStats: &system.NetIoStats{},
|
||||||
|
dockerClient: newDockerClient(),
|
||||||
|
bufferPool: &sync.Pool{
|
||||||
|
New: func() interface{} {
|
||||||
|
return new(bytes.Buffer)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Agent) acquireSemaphore() {
|
||||||
|
a.sem <- struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Agent) releaseSemaphore() {
|
||||||
|
<-a.sem
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Agent) getSystemStats() (system.Info, system.Stats) {
|
||||||
|
systemStats := system.Stats{}
|
||||||
|
|
||||||
|
// cpu percent
|
||||||
|
cpuPct, err := cpu.Percent(0, false)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("Error getting cpu percent:", err)
|
||||||
|
} else if len(cpuPct) > 0 {
|
||||||
|
systemStats.Cpu = twoDecimals(cpuPct[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
// memory
|
||||||
|
if v, err := mem.VirtualMemory(); err == nil {
|
||||||
|
systemStats.Mem = bytesToGigabytes(v.Total)
|
||||||
|
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.SwapUsed = bytesToGigabytes(v.SwapTotal - v.SwapFree)
|
||||||
|
}
|
||||||
|
|
||||||
|
// disk usage
|
||||||
|
for _, stats := range a.fsStats {
|
||||||
|
// 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.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
|
||||||
|
if ioCounters, err := disk.IOCounters(a.fsNames...); err == nil {
|
||||||
|
for _, d := range ioCounters {
|
||||||
|
stats := a.fsStats[d.Name]
|
||||||
|
if stats == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
secondsElapsed := time.Since(stats.Time).Seconds()
|
||||||
|
readPerSecond := float64(d.ReadBytes-stats.TotalRead) / secondsElapsed
|
||||||
|
writePerSecond := float64(d.WriteBytes-stats.TotalWrite) / secondsElapsed
|
||||||
|
stats.Time = time.Now()
|
||||||
|
stats.DiskReadPs = bytesToMegabytes(readPerSecond)
|
||||||
|
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
|
||||||
|
if netIO, err := psutilNet.IOCounters(true); err == nil {
|
||||||
|
secondsElapsed := time.Since(a.netIoStats.Time).Seconds()
|
||||||
|
a.netIoStats.Time = time.Now()
|
||||||
|
bytesSent := uint64(0)
|
||||||
|
bytesRecv := uint64(0)
|
||||||
|
// sum all bytes sent and received
|
||||||
|
for _, v := range netIO {
|
||||||
|
// skip if not in valid network interfaces list
|
||||||
|
if _, exists := a.netInterfaces[v.Name]; !exists {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// log.Printf("%+v: %+v recv, %+v sent\n", v.Name, v.BytesRecv, v.BytesSent)
|
||||||
|
bytesSent += v.BytesSent
|
||||||
|
bytesRecv += v.BytesRecv
|
||||||
|
}
|
||||||
|
// add to systemStats
|
||||||
|
sentPerSecond := float64(bytesSent-a.netIoStats.BytesSent) / secondsElapsed
|
||||||
|
recvPerSecond := float64(bytesRecv-a.netIoStats.BytesRecv) / secondsElapsed
|
||||||
|
networkSentPs := bytesToMegabytes(sentPerSecond)
|
||||||
|
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
|
||||||
|
a.netIoStats.BytesSent = bytesSent
|
||||||
|
a.netIoStats.BytesRecv = bytesRecv
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// temperatures
|
||||||
|
if temps, err := sensors.SensorsTemperatures(); err == nil {
|
||||||
|
systemStats.Temperatures = make(map[string]float64)
|
||||||
|
// log.Printf("Temperatures: %+v\n", temps)
|
||||||
|
for i, temp := range temps {
|
||||||
|
if _, ok := systemStats.Temperatures[temp.SensorKey]; ok {
|
||||||
|
// if key already exists, append int to key
|
||||||
|
systemStats.Temperatures[temp.SensorKey+"_"+strconv.Itoa(i)] = twoDecimals(temp.Temperature)
|
||||||
|
} else {
|
||||||
|
systemStats.Temperatures[temp.SensorKey] = twoDecimals(temp.Temperature)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// log.Printf("Temperature map: %+v\n", systemStats.Temperatures)
|
||||||
|
}
|
||||||
|
|
||||||
|
systemInfo := system.Info{
|
||||||
|
Cpu: systemStats.Cpu,
|
||||||
|
MemPct: systemStats.MemPct,
|
||||||
|
DiskPct: systemStats.DiskPct,
|
||||||
|
AgentVersion: beszel.Version,
|
||||||
|
}
|
||||||
|
|
||||||
|
// add host info
|
||||||
|
if info, err := host.Info(); err == nil {
|
||||||
|
systemInfo.Uptime = info.Uptime
|
||||||
|
// systemInfo.Os = info.OS
|
||||||
|
}
|
||||||
|
// add cpu stats
|
||||||
|
if info, err := cpu.Info(); err == nil && len(info) > 0 {
|
||||||
|
systemInfo.CpuModel = info[0].ModelName
|
||||||
|
}
|
||||||
|
if cores, err := cpu.Counts(false); err == nil {
|
||||||
|
systemInfo.Cores = cores
|
||||||
|
}
|
||||||
|
if threads, err := cpu.Counts(true); err == nil {
|
||||||
|
systemInfo.Threads = threads
|
||||||
|
}
|
||||||
|
|
||||||
|
return systemInfo, systemStats
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Agent) getDockerStats() ([]container.Stats, error) {
|
||||||
|
resp, err := a.dockerClient.Get("http://localhost/containers/json")
|
||||||
|
if err != nil {
|
||||||
|
a.closeIdleConnections(err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
var containers []container.ApiInfo
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&containers); err != nil {
|
||||||
|
log.Printf("Error decoding containers: %+v\n", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
containerStats := make([]container.Stats, 0, len(containers))
|
||||||
|
containerStatsMutex := sync.Mutex{}
|
||||||
|
|
||||||
|
// store valid ids to clean up old container ids from map
|
||||||
|
validIds := make(map[string]struct{}, len(containers))
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
|
||||||
|
for _, ctr := range containers {
|
||||||
|
ctr.IdShort = ctr.Id[:12]
|
||||||
|
validIds[ctr.IdShort] = struct{}{}
|
||||||
|
// check if container is less than 1 minute old (possible restart)
|
||||||
|
// note: can't use Created field because it's not updated on restart
|
||||||
|
if strings.Contains(ctr.Status, "second") {
|
||||||
|
// if so, remove old container data
|
||||||
|
a.deleteContainerStatsSync(ctr.IdShort)
|
||||||
|
}
|
||||||
|
wg.Add(1)
|
||||||
|
a.acquireSemaphore()
|
||||||
|
go func() {
|
||||||
|
defer a.releaseSemaphore()
|
||||||
|
defer wg.Done()
|
||||||
|
cstats, err := a.getContainerStats(ctr)
|
||||||
|
if err != nil {
|
||||||
|
// close idle connections if error is a network timeout
|
||||||
|
isTimeout := a.closeIdleConnections(err)
|
||||||
|
// delete container from map if not a timeout
|
||||||
|
if !isTimeout {
|
||||||
|
a.deleteContainerStatsSync(ctr.IdShort)
|
||||||
|
}
|
||||||
|
// retry once
|
||||||
|
cstats, err = a.getContainerStats(ctr)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error getting container stats: %+v\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
containerStatsMutex.Lock()
|
||||||
|
defer containerStatsMutex.Unlock()
|
||||||
|
containerStats = append(containerStats, cstats)
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
|
for id := range a.containerStatsMap {
|
||||||
|
if _, exists := validIds[id]; !exists {
|
||||||
|
// log.Printf("Removing container cpu map entry: %+v\n", id)
|
||||||
|
delete(a.containerStatsMap, id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return containerStats, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Agent) getContainerStats(ctr container.ApiInfo) (container.Stats, error) {
|
||||||
|
cStats := container.Stats{}
|
||||||
|
|
||||||
|
resp, err := a.dockerClient.Get("http://localhost/containers/" + ctr.IdShort + "/stats?stream=0&one-shot=1")
|
||||||
|
if err != nil {
|
||||||
|
return cStats, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
// use a pooled buffer to store the response body
|
||||||
|
buf := a.bufferPool.Get().(*bytes.Buffer)
|
||||||
|
defer a.bufferPool.Put(buf)
|
||||||
|
buf.Reset()
|
||||||
|
_, err = io.Copy(buf, resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return cStats, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// unmarshal the json data from the buffer
|
||||||
|
var statsJson container.ApiStats
|
||||||
|
if err := json.Unmarshal(buf.Bytes(), &statsJson); err != nil {
|
||||||
|
return cStats, err
|
||||||
|
}
|
||||||
|
|
||||||
|
name := ctr.Names[0][1:]
|
||||||
|
|
||||||
|
// check if container has valid data, otherwise may be in restart loop (#103)
|
||||||
|
if statsJson.MemoryStats.Usage == 0 {
|
||||||
|
return cStats, fmt.Errorf("%s - invalid data", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// memory (https://docs.docker.com/reference/cli/docker/container/stats/)
|
||||||
|
memCache := statsJson.MemoryStats.Stats["inactive_file"]
|
||||||
|
if memCache == 0 {
|
||||||
|
memCache = statsJson.MemoryStats.Stats["cache"]
|
||||||
|
}
|
||||||
|
usedMemory := statsJson.MemoryStats.Usage - memCache
|
||||||
|
|
||||||
|
a.containerStatsMutex.Lock()
|
||||||
|
defer a.containerStatsMutex.Unlock()
|
||||||
|
|
||||||
|
// add empty values if they doesn't exist in map
|
||||||
|
stats, initialized := a.containerStatsMap[ctr.IdShort]
|
||||||
|
if !initialized {
|
||||||
|
stats = &container.PrevContainerStats{}
|
||||||
|
a.containerStatsMap[ctr.IdShort] = stats
|
||||||
|
}
|
||||||
|
|
||||||
|
// cpu
|
||||||
|
cpuDelta := statsJson.CPUStats.CPUUsage.TotalUsage - stats.Cpu[0]
|
||||||
|
systemDelta := statsJson.CPUStats.SystemUsage - stats.Cpu[1]
|
||||||
|
cpuPct := float64(cpuDelta) / float64(systemDelta) * 100
|
||||||
|
if cpuPct > 100 {
|
||||||
|
return cStats, fmt.Errorf("%s cpu pct greater than 100: %+v", name, cpuPct)
|
||||||
|
}
|
||||||
|
stats.Cpu = [2]uint64{statsJson.CPUStats.CPUUsage.TotalUsage, statsJson.CPUStats.SystemUsage}
|
||||||
|
|
||||||
|
// network
|
||||||
|
var total_sent, total_recv uint64
|
||||||
|
for _, v := range statsJson.Networks {
|
||||||
|
total_sent += v.TxBytes
|
||||||
|
total_recv += v.RxBytes
|
||||||
|
}
|
||||||
|
var sent_delta, recv_delta float64
|
||||||
|
// prevent first run from sending all prev sent/recv bytes
|
||||||
|
if initialized {
|
||||||
|
secondsElapsed := time.Since(stats.Net.Time).Seconds()
|
||||||
|
sent_delta = float64(total_sent-stats.Net.Sent) / secondsElapsed
|
||||||
|
recv_delta = float64(total_recv-stats.Net.Recv) / secondsElapsed
|
||||||
|
// log.Printf("sent delta: %+v, recv delta: %+v\n", sent_delta, recv_delta)
|
||||||
|
}
|
||||||
|
stats.Net.Sent = total_sent
|
||||||
|
stats.Net.Recv = total_recv
|
||||||
|
stats.Net.Time = time.Now()
|
||||||
|
|
||||||
|
// cStats := a.containerStatsPool.Get().(*container.Stats)
|
||||||
|
cStats.Name = name
|
||||||
|
cStats.Cpu = twoDecimals(cpuPct)
|
||||||
|
cStats.Mem = bytesToMegabytes(float64(usedMemory))
|
||||||
|
cStats.NetworkSent = bytesToMegabytes(sent_delta)
|
||||||
|
cStats.NetworkRecv = bytesToMegabytes(recv_delta)
|
||||||
|
|
||||||
|
return cStats, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 (a *Agent) gatherStats() system.CombinedData {
|
||||||
|
systemInfo, systemStats := a.getSystemStats()
|
||||||
|
systemData := system.CombinedData{
|
||||||
|
Stats: systemStats,
|
||||||
|
Info: systemInfo,
|
||||||
|
}
|
||||||
|
// add docker stats
|
||||||
|
if containerStats, err := a.getDockerStats(); err == nil {
|
||||||
|
systemData.Containers = containerStats
|
||||||
|
}
|
||||||
|
// add extra filesystems
|
||||||
|
systemData.Stats.ExtraFs = make(map[string]*system.FsStats)
|
||||||
|
for name, stats := range a.fsStats {
|
||||||
|
if !stats.Root && stats.DiskTotal > 0 {
|
||||||
|
systemData.Stats.ExtraFs[name] = stats
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// log.Printf("%+v\n", systemData)
|
||||||
|
return systemData
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Agent) startServer() {
|
||||||
|
sshServer.Handle(a.handleSession)
|
||||||
|
|
||||||
|
log.Printf("Starting SSH server on %s", a.addr)
|
||||||
|
if err := sshServer.ListenAndServe(a.addr, nil, sshServer.NoPty(),
|
||||||
|
sshServer.PublicKeyAuth(func(ctx sshServer.Context, key sshServer.PublicKey) bool {
|
||||||
|
allowed, _, _, _, _ := sshServer.ParseAuthorizedKey(a.pubKey)
|
||||||
|
return sshServer.KeysEqual(key, allowed)
|
||||||
|
}),
|
||||||
|
); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Agent) handleSession(s sshServer.Session) {
|
||||||
|
stats := a.gatherStats()
|
||||||
|
encoder := json.NewEncoder(s)
|
||||||
|
if err := encoder.Encode(stats); err != nil {
|
||||||
|
log.Println("Error encoding stats:", err.Error())
|
||||||
|
s.Exit(1)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s.Exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Agent) Run() {
|
||||||
|
a.fsStats = make(map[string]*system.FsStats)
|
||||||
|
|
||||||
|
filesystem, fsEnvVarExists := os.LookupEnv("FILESYSTEM")
|
||||||
|
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.initializeNetIoStats()
|
||||||
|
|
||||||
|
// log.Printf("Filesystems: %+v\n", a.fsStats)
|
||||||
|
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() {
|
||||||
|
// 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 {
|
||||||
|
// add name to slice
|
||||||
|
a.fsNames = append(a.fsNames, d.Name)
|
||||||
|
// 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() {
|
||||||
|
// 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 {
|
||||||
|
a.netIoStats.Time = time.Now()
|
||||||
|
for _, v := range netIO {
|
||||||
|
if skipNetworkInterface(v) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
log.Printf("Detected network interface: %+v (%+v recv, %+v sent)\n", v.Name, v.BytesRecv, v.BytesSent)
|
||||||
|
a.netIoStats.BytesSent += v.BytesSent
|
||||||
|
a.netIoStats.BytesRecv += v.BytesRecv
|
||||||
|
// store as a valid network interface
|
||||||
|
a.netInterfaces[v.Name] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func bytesToMegabytes(b float64) float64 {
|
||||||
|
return twoDecimals(b / 1048576)
|
||||||
|
}
|
||||||
|
|
||||||
|
func bytesToGigabytes(b uint64) float64 {
|
||||||
|
return twoDecimals(float64(b) / 1073741824)
|
||||||
|
}
|
||||||
|
|
||||||
|
func twoDecimals(value float64) float64 {
|
||||||
|
return math.Round(value*100) / 100
|
||||||
|
}
|
||||||
|
|
||||||
|
func skipNetworkInterface(v psutilNet.IOCountersStat) bool {
|
||||||
|
switch {
|
||||||
|
case strings.HasPrefix(v.Name, "lo"),
|
||||||
|
strings.HasPrefix(v.Name, "docker"),
|
||||||
|
strings.HasPrefix(v.Name, "br-"),
|
||||||
|
strings.HasPrefix(v.Name, "veth"),
|
||||||
|
v.BytesRecv == 0,
|
||||||
|
v.BytesSent == 0:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newDockerClient() *http.Client {
|
||||||
|
dockerHost := "unix:///var/run/docker.sock"
|
||||||
|
if dockerHostEnv, exists := os.LookupEnv("DOCKER_HOST"); exists {
|
||||||
|
dockerHost = dockerHostEnv
|
||||||
|
}
|
||||||
|
|
||||||
|
parsedURL, err := url.Parse(dockerHost)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("Error parsing DOCKER_HOST: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
transport := &http.Transport{
|
||||||
|
ForceAttemptHTTP2: false,
|
||||||
|
IdleConnTimeout: 90 * time.Second,
|
||||||
|
DisableCompression: true,
|
||||||
|
MaxConnsPerHost: 20,
|
||||||
|
MaxIdleConnsPerHost: 20,
|
||||||
|
DisableKeepAlives: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
switch parsedURL.Scheme {
|
||||||
|
case "unix":
|
||||||
|
transport.DialContext = func(ctx context.Context, proto, addr string) (net.Conn, error) {
|
||||||
|
return (&net.Dialer{}).DialContext(ctx, "unix", parsedURL.Path)
|
||||||
|
}
|
||||||
|
case "tcp", "http", "https":
|
||||||
|
log.Println("Using DOCKER_HOST: " + dockerHost)
|
||||||
|
transport.DialContext = func(ctx context.Context, proto, addr string) (net.Conn, error) {
|
||||||
|
return (&net.Dialer{}).DialContext(ctx, "tcp", parsedURL.Host)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
log.Fatal("Unsupported DOCKER_HOST: " + parsedURL.Scheme)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &http.Client{
|
||||||
|
Timeout: time.Second,
|
||||||
|
Transport: transport,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// closes idle connections on timeouts to prevent reuse of stale connections
|
||||||
|
func (a *Agent) closeIdleConnections(err error) (isTimeout bool) {
|
||||||
|
if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
|
||||||
|
log.Printf("Closing idle connections. Error: %+v\n", err)
|
||||||
|
a.dockerClient.Transport.(*http.Transport).CloseIdleConnections()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
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
|
||||||
|
}
|
||||||
151
beszel/internal/alerts/alerts.go
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
// Package alerts handles alert management and delivery.
|
||||||
|
package alerts
|
||||||
|
|
||||||
|
import (
|
||||||
|
"beszel/internal/entities/system"
|
||||||
|
"fmt"
|
||||||
|
"net/mail"
|
||||||
|
|
||||||
|
"github.com/pocketbase/dbx"
|
||||||
|
"github.com/pocketbase/pocketbase"
|
||||||
|
"github.com/pocketbase/pocketbase/models"
|
||||||
|
"github.com/pocketbase/pocketbase/tools/mailer"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AlertManager struct {
|
||||||
|
app *pocketbase.PocketBase
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAlertManager(app *pocketbase.PocketBase) *AlertManager {
|
||||||
|
return &AlertManager{
|
||||||
|
app: app,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (am *AlertManager) HandleSystemAlerts(newStatus string, newRecord *models.Record, oldRecord *models.Record) {
|
||||||
|
alertRecords, err := am.app.Dao().FindRecordsByExpr("alerts",
|
||||||
|
dbx.NewExp("system = {:system}", dbx.Params{"system": oldRecord.GetId()}),
|
||||||
|
)
|
||||||
|
if err != nil || len(alertRecords) == 0 {
|
||||||
|
// log.Println("no alerts found for system")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// log.Println("found alerts", len(alertRecords))
|
||||||
|
var systemInfo *system.Info
|
||||||
|
for _, alertRecord := range alertRecords {
|
||||||
|
name := alertRecord.GetString("name")
|
||||||
|
switch name {
|
||||||
|
case "Status":
|
||||||
|
am.handleStatusAlerts(newStatus, oldRecord, alertRecord)
|
||||||
|
case "CPU", "Memory", "Disk":
|
||||||
|
if newStatus != "up" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if systemInfo == nil {
|
||||||
|
systemInfo = getSystemInfo(newRecord)
|
||||||
|
}
|
||||||
|
if name == "CPU" {
|
||||||
|
am.handleSlidingValueAlert(newRecord, alertRecord, name, systemInfo.Cpu)
|
||||||
|
} else if name == "Memory" {
|
||||||
|
am.handleSlidingValueAlert(newRecord, alertRecord, name, systemInfo.MemPct)
|
||||||
|
} else if name == "Disk" {
|
||||||
|
am.handleSlidingValueAlert(newRecord, alertRecord, name, systemInfo.DiskPct)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getSystemInfo(record *models.Record) *system.Info {
|
||||||
|
var SystemInfo system.Info
|
||||||
|
record.UnmarshalJSONField("info", &SystemInfo)
|
||||||
|
return &SystemInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
func (am *AlertManager) handleSlidingValueAlert(newRecord *models.Record, alertRecord *models.Record, name string, curValue float64) {
|
||||||
|
triggered := alertRecord.GetBool("triggered")
|
||||||
|
threshold := alertRecord.GetFloat("value")
|
||||||
|
// fmt.Println(name, curValue, "threshold", threshold, "triggered", triggered)
|
||||||
|
var subject string
|
||||||
|
var body string
|
||||||
|
if !triggered && curValue > threshold {
|
||||||
|
alertRecord.Set("triggered", true)
|
||||||
|
systemName := newRecord.GetString("name")
|
||||||
|
subject = fmt.Sprintf("%s usage above threshold on %s", name, systemName)
|
||||||
|
body = fmt.Sprintf("%s usage on %s is %.1f%%.\n\n%s\n\n- Beszel", name, systemName, curValue, am.app.Settings().Meta.AppUrl+"/system/"+systemName)
|
||||||
|
} else if triggered && curValue <= threshold {
|
||||||
|
alertRecord.Set("triggered", false)
|
||||||
|
systemName := newRecord.GetString("name")
|
||||||
|
subject = fmt.Sprintf("%s usage below threshold on %s", name, systemName)
|
||||||
|
body = fmt.Sprintf("%s usage on %s is below threshold at %.1f%%.\n\n%s\n\n- Beszel", name, systemName, curValue, am.app.Settings().Meta.AppUrl+"/system/"+systemName)
|
||||||
|
} else {
|
||||||
|
// fmt.Println(name, "not triggered")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := am.app.Dao().SaveRecord(alertRecord); err != nil {
|
||||||
|
// app.Logger().Error("failed to save alert record", "err", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// expand the user relation and send the alert
|
||||||
|
if errs := am.app.Dao().ExpandRecord(alertRecord, []string{"user"}, nil); len(errs) > 0 {
|
||||||
|
// app.Logger().Error("failed to expand user relation", "errs", errs)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if user := alertRecord.ExpandedOne("user"); user != nil {
|
||||||
|
am.sendAlert(&mailer.Message{
|
||||||
|
To: []mail.Address{{Address: user.GetString("email")}},
|
||||||
|
Subject: subject,
|
||||||
|
Text: body,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (am *AlertManager) handleStatusAlerts(newStatus string, oldRecord *models.Record, alertRecord *models.Record) error {
|
||||||
|
var alertStatus string
|
||||||
|
switch newStatus {
|
||||||
|
case "up":
|
||||||
|
if oldRecord.GetString("status") == "down" {
|
||||||
|
alertStatus = "up"
|
||||||
|
}
|
||||||
|
case "down":
|
||||||
|
if oldRecord.GetString("status") == "up" {
|
||||||
|
alertStatus = "down"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if alertStatus == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// expand the user relation
|
||||||
|
if errs := am.app.Dao().ExpandRecord(alertRecord, []string{"user"}, nil); len(errs) > 0 {
|
||||||
|
return fmt.Errorf("failed to expand: %v", errs)
|
||||||
|
}
|
||||||
|
user := alertRecord.ExpandedOne("user")
|
||||||
|
if user == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
emoji := "\U0001F534"
|
||||||
|
if alertStatus == "up" {
|
||||||
|
emoji = "\u2705"
|
||||||
|
}
|
||||||
|
// send alert
|
||||||
|
systemName := oldRecord.GetString("name")
|
||||||
|
am.sendAlert(&mailer.Message{
|
||||||
|
To: []mail.Address{{Address: user.GetString("email")}},
|
||||||
|
Subject: fmt.Sprintf("Connection to %s is %s %v", systemName, alertStatus, emoji),
|
||||||
|
Text: fmt.Sprintf("Connection to %s is %s\n\n- Beszel", systemName, alertStatus),
|
||||||
|
})
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (am *AlertManager) sendAlert(message *mailer.Message) {
|
||||||
|
// fmt.Println("sending alert", "to", message.To, "subj", message.Subject, "body", message.Text)
|
||||||
|
message.From = mail.Address{
|
||||||
|
Address: am.app.Settings().Meta.SenderAddress,
|
||||||
|
Name: am.app.Settings().Meta.SenderName,
|
||||||
|
}
|
||||||
|
if err := am.app.NewMailClient().Send(message); err != nil {
|
||||||
|
am.app.Logger().Error("Failed to send alert: ", "err", err.Error())
|
||||||
|
} else {
|
||||||
|
am.app.Logger().Info("Sent alert", "to", message.To, "subj", message.Subject)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,50 +1,9 @@
|
|||||||
package main
|
package container
|
||||||
|
|
||||||
import "time"
|
import "time"
|
||||||
|
|
||||||
type SystemData struct {
|
// Docker container info from /containers/json
|
||||||
Stats *SystemStats `json:"stats"`
|
type ApiInfo struct {
|
||||||
Info *SystemInfo `json:"info"`
|
|
||||||
Containers []*ContainerStats `json:"container"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type SystemInfo struct {
|
|
||||||
Cores int `json:"c"`
|
|
||||||
Threads int `json:"t"`
|
|
||||||
CpuModel string `json:"m"`
|
|
||||||
// Os string `json:"o"`
|
|
||||||
Uptime uint64 `json:"u"`
|
|
||||||
Cpu float64 `json:"cpu"`
|
|
||||||
MemPct float64 `json:"mp"`
|
|
||||||
DiskPct float64 `json:"dp"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type SystemStats struct {
|
|
||||||
Cpu float64 `json:"cpu"`
|
|
||||||
Mem float64 `json:"m"`
|
|
||||||
MemUsed float64 `json:"mu"`
|
|
||||||
MemPct float64 `json:"mp"`
|
|
||||||
MemBuffCache float64 `json:"mb"`
|
|
||||||
Swap float64 `json:"s"`
|
|
||||||
SwapUsed float64 `json:"su"`
|
|
||||||
Disk float64 `json:"d"`
|
|
||||||
DiskUsed float64 `json:"du"`
|
|
||||||
DiskPct float64 `json:"dp"`
|
|
||||||
DiskRead float64 `json:"dr"`
|
|
||||||
DiskWrite float64 `json:"dw"`
|
|
||||||
NetworkSent float64 `json:"ns"`
|
|
||||||
NetworkRecv float64 `json:"nr"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ContainerStats struct {
|
|
||||||
Name string `json:"n"`
|
|
||||||
Cpu float64 `json:"c"`
|
|
||||||
Mem float64 `json:"m"`
|
|
||||||
NetworkSent float64 `json:"ns"`
|
|
||||||
NetworkRecv float64 `json:"nr"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Container struct {
|
|
||||||
Id string
|
Id string
|
||||||
IdShort string
|
IdShort string
|
||||||
Names []string
|
Names []string
|
||||||
@@ -66,7 +25,8 @@ type Container struct {
|
|||||||
// Mounts []MountPoint
|
// Mounts []MountPoint
|
||||||
}
|
}
|
||||||
|
|
||||||
type CStats struct {
|
// Docker container resources from /containers/{id}/stats
|
||||||
|
type ApiStats struct {
|
||||||
// Common stats
|
// Common stats
|
||||||
// Read time.Time `json:"read"`
|
// Read time.Time `json:"read"`
|
||||||
// PreRead time.Time `json:"preread"`
|
// PreRead time.Time `json:"preread"`
|
||||||
@@ -153,20 +113,16 @@ type NetworkStats struct {
|
|||||||
TxBytes uint64 `json:"tx_bytes"`
|
TxBytes uint64 `json:"tx_bytes"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type DiskIoStats struct {
|
// Container stats to return to the hub
|
||||||
Read uint64
|
type Stats struct {
|
||||||
Write uint64
|
Name string `json:"n"`
|
||||||
Time time.Time
|
Cpu float64 `json:"c"`
|
||||||
Filesystem string
|
Mem float64 `json:"m"`
|
||||||
}
|
NetworkSent float64 `json:"ns"`
|
||||||
|
NetworkRecv float64 `json:"nr"`
|
||||||
type NetIoStats struct {
|
|
||||||
BytesRecv uint64
|
|
||||||
BytesSent uint64
|
|
||||||
Time time.Time
|
|
||||||
Name string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Keeps track of container stats from previous run
|
||||||
type PrevContainerStats struct {
|
type PrevContainerStats struct {
|
||||||
Cpu [2]uint64
|
Cpu [2]uint64
|
||||||
Net struct {
|
Net struct {
|
||||||
64
beszel/internal/entities/system/system.go
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
package system
|
||||||
|
|
||||||
|
import (
|
||||||
|
"beszel/internal/entities/container"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Stats struct {
|
||||||
|
Cpu float64 `json:"cpu"`
|
||||||
|
Mem float64 `json:"m"`
|
||||||
|
MemUsed float64 `json:"mu"`
|
||||||
|
MemPct float64 `json:"mp"`
|
||||||
|
MemBuffCache float64 `json:"mb"`
|
||||||
|
Swap float64 `json:"s,omitempty"`
|
||||||
|
SwapUsed float64 `json:"su,omitempty"`
|
||||||
|
DiskTotal float64 `json:"d"`
|
||||||
|
DiskUsed float64 `json:"du"`
|
||||||
|
DiskPct float64 `json:"dp"`
|
||||||
|
DiskReadPs float64 `json:"dr"`
|
||||||
|
DiskWritePs float64 `json:"dw"`
|
||||||
|
NetworkSent float64 `json:"ns"`
|
||||||
|
NetworkRecv float64 `json:"nr"`
|
||||||
|
Temperatures map[string]float64 `json:"t,omitempty"`
|
||||||
|
ExtraFs map[string]*FsStats `json:"efs,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type FsStats struct {
|
||||||
|
Time time.Time `json:"-"`
|
||||||
|
Device string `json:"-"`
|
||||||
|
Root bool `json:"-"`
|
||||||
|
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 {
|
||||||
|
BytesRecv uint64
|
||||||
|
BytesSent uint64
|
||||||
|
Time time.Time
|
||||||
|
Name string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Info struct {
|
||||||
|
Cores int `json:"c"`
|
||||||
|
Threads int `json:"t"`
|
||||||
|
CpuModel string `json:"m"`
|
||||||
|
// Os string `json:"o"`
|
||||||
|
Uptime uint64 `json:"u"`
|
||||||
|
Cpu float64 `json:"cpu"`
|
||||||
|
MemPct float64 `json:"mp"`
|
||||||
|
DiskPct float64 `json:"dp"`
|
||||||
|
AgentVersion string `json:"v"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Final data structure to return to the hub
|
||||||
|
type CombinedData struct {
|
||||||
|
Stats Stats `json:"stats"`
|
||||||
|
Info Info `json:"info"`
|
||||||
|
Containers []container.Stats `json:"container"`
|
||||||
|
}
|
||||||
477
beszel/internal/hub/hub.go
Normal file
@@ -0,0 +1,477 @@
|
|||||||
|
package hub
|
||||||
|
|
||||||
|
import (
|
||||||
|
"beszel"
|
||||||
|
"beszel/internal/alerts"
|
||||||
|
"beszel/internal/entities/system"
|
||||||
|
"beszel/internal/records"
|
||||||
|
"beszel/site"
|
||||||
|
"context"
|
||||||
|
"crypto/ed25519"
|
||||||
|
"encoding/pem"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httputil"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/goccy/go-json"
|
||||||
|
|
||||||
|
"github.com/labstack/echo/v5"
|
||||||
|
"github.com/pocketbase/pocketbase"
|
||||||
|
"github.com/pocketbase/pocketbase/apis"
|
||||||
|
"github.com/pocketbase/pocketbase/core"
|
||||||
|
"github.com/pocketbase/pocketbase/models"
|
||||||
|
"github.com/pocketbase/pocketbase/plugins/migratecmd"
|
||||||
|
"github.com/pocketbase/pocketbase/tools/cron"
|
||||||
|
"golang.org/x/crypto/ssh"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Hub struct {
|
||||||
|
app *pocketbase.PocketBase
|
||||||
|
connectionLock *sync.Mutex
|
||||||
|
systemConnections map[string]*ssh.Client
|
||||||
|
sshClientConfig *ssh.ClientConfig
|
||||||
|
pubKey string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewHub(app *pocketbase.PocketBase) *Hub {
|
||||||
|
return &Hub{
|
||||||
|
app: app,
|
||||||
|
connectionLock: &sync.Mutex{},
|
||||||
|
systemConnections: make(map[string]*ssh.Client),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Hub) Run() {
|
||||||
|
var rm *records.RecordManager
|
||||||
|
var am *alerts.AlertManager
|
||||||
|
|
||||||
|
// loosely check if it was executed using "go run"
|
||||||
|
isGoRun := strings.HasPrefix(os.Args[0], os.TempDir())
|
||||||
|
|
||||||
|
// // enable auto creation of migration files when making collection changes in the Admin UI
|
||||||
|
migratecmd.MustRegister(h.app, h.app.RootCmd, migratecmd.Config{
|
||||||
|
// (the isGoRun check is to enable it only during development)
|
||||||
|
Automigrate: isGoRun,
|
||||||
|
Dir: "../../migrations",
|
||||||
|
})
|
||||||
|
|
||||||
|
// initial setup
|
||||||
|
h.app.OnBeforeServe().Add(func(e *core.ServeEvent) error {
|
||||||
|
// set up record manager and alert manager
|
||||||
|
rm = records.NewRecordManager(h.app)
|
||||||
|
am = alerts.NewAlertManager(h.app)
|
||||||
|
// create ssh client config
|
||||||
|
err := h.createSSHClientConfig()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
// set auth settings
|
||||||
|
usersCollection, err := h.app.Dao().FindCollectionByNameOrId("users")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
usersAuthOptions := usersCollection.AuthOptions()
|
||||||
|
usersAuthOptions.AllowUsernameAuth = false
|
||||||
|
if os.Getenv("DISABLE_PASSWORD_AUTH") == "true" {
|
||||||
|
usersAuthOptions.AllowEmailAuth = false
|
||||||
|
} else {
|
||||||
|
usersAuthOptions.AllowEmailAuth = true
|
||||||
|
}
|
||||||
|
usersCollection.SetOptions(usersAuthOptions)
|
||||||
|
if err := h.app.Dao().SaveCollection(usersCollection); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
// serve site
|
||||||
|
h.app.OnBeforeServe().Add(func(e *core.ServeEvent) error {
|
||||||
|
switch isGoRun {
|
||||||
|
case true:
|
||||||
|
proxy := httputil.NewSingleHostReverseProxy(&url.URL{
|
||||||
|
Scheme: "http",
|
||||||
|
Host: "localhost:5173",
|
||||||
|
})
|
||||||
|
e.Router.GET("/static/*", apis.StaticDirectoryHandler(os.DirFS("../../site/public/static"), false))
|
||||||
|
e.Router.Any("/*", echo.WrapHandler(proxy))
|
||||||
|
// e.Router.Any("/", echo.WrapHandler(proxy))
|
||||||
|
default:
|
||||||
|
e.Router.GET("/static/*", apis.StaticDirectoryHandler(site.Static, false))
|
||||||
|
e.Router.Any("/*", apis.StaticDirectoryHandler(site.Dist, true))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
// set up scheduled jobs / ticker for system updates
|
||||||
|
h.app.OnBeforeServe().Add(func(e *core.ServeEvent) error {
|
||||||
|
// 15 second ticker for system updates
|
||||||
|
go h.startSystemUpdateTicker()
|
||||||
|
// set up cron jobs
|
||||||
|
scheduler := cron.New()
|
||||||
|
// delete old records once every hour
|
||||||
|
scheduler.MustAdd("delete old records", "8 * * * *", rm.DeleteOldRecords)
|
||||||
|
// create longer records every 10 minutes
|
||||||
|
scheduler.MustAdd("create longer records", "*/10 * * * *", rm.CreateLongerRecords)
|
||||||
|
scheduler.Start()
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
// custom api routes
|
||||||
|
h.app.OnBeforeServe().Add(func(e *core.ServeEvent) error {
|
||||||
|
// returns public key
|
||||||
|
e.Router.GET("/api/beszel/getkey", func(c echo.Context) error {
|
||||||
|
requestData := apis.RequestInfo(c)
|
||||||
|
if requestData.AuthRecord == nil {
|
||||||
|
return apis.NewForbiddenError("Forbidden", nil)
|
||||||
|
}
|
||||||
|
return c.JSON(http.StatusOK, map[string]string{"key": h.pubKey, "v": beszel.Version})
|
||||||
|
})
|
||||||
|
// check if first time setup on login page
|
||||||
|
e.Router.GET("/api/beszel/first-run", func(c echo.Context) error {
|
||||||
|
adminNum, err := h.app.Dao().TotalAdmins()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return c.JSON(http.StatusOK, map[string]bool{"firstRun": adminNum == 0})
|
||||||
|
})
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
// user creation - set default role to user if unset
|
||||||
|
h.app.OnModelBeforeCreate("users").Add(func(e *core.ModelEvent) error {
|
||||||
|
user := e.Model.(*models.Record)
|
||||||
|
if user.GetString("role") == "" {
|
||||||
|
user.Set("role", "user")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
// system creation defaults
|
||||||
|
h.app.OnModelBeforeCreate("systems").Add(func(e *core.ModelEvent) error {
|
||||||
|
record := e.Model.(*models.Record)
|
||||||
|
record.Set("info", system.Info{})
|
||||||
|
record.Set("status", "pending")
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
// immediately create connection for new systems
|
||||||
|
h.app.OnModelAfterCreate("systems").Add(func(e *core.ModelEvent) error {
|
||||||
|
go h.updateSystem(e.Model.(*models.Record))
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
// do things after a systems record is updated
|
||||||
|
h.app.OnModelAfterUpdate("systems").Add(func(e *core.ModelEvent) error {
|
||||||
|
newRecord := e.Model.(*models.Record)
|
||||||
|
oldRecord := newRecord.OriginalCopy()
|
||||||
|
newStatus := newRecord.GetString("status")
|
||||||
|
|
||||||
|
// if system is disconnected and connection exists, remove it
|
||||||
|
if newStatus == "down" || newStatus == "paused" {
|
||||||
|
h.deleteSystemConnection(newRecord)
|
||||||
|
}
|
||||||
|
|
||||||
|
// if system is set to pending (unpause), try to connect immediately
|
||||||
|
if newStatus == "pending" {
|
||||||
|
go h.updateSystem(newRecord)
|
||||||
|
}
|
||||||
|
|
||||||
|
// alerts
|
||||||
|
am.HandleSystemAlerts(newStatus, newRecord, oldRecord)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
// do things after a systems record is deleted
|
||||||
|
h.app.OnModelAfterDelete("systems").Add(func(e *core.ModelEvent) error {
|
||||||
|
// if system connection exists, close it
|
||||||
|
h.deleteSystemConnection(e.Model.(*models.Record))
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if err := h.app.Start(); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Hub) startSystemUpdateTicker() {
|
||||||
|
ticker := time.NewTicker(15 * time.Second)
|
||||||
|
for range ticker.C {
|
||||||
|
h.updateSystems()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Hub) updateSystems() {
|
||||||
|
records, err := h.app.Dao().FindRecordsByFilter(
|
||||||
|
"2hz5ncl8tizk5nx", // systems collection
|
||||||
|
"status != 'paused'", // filter
|
||||||
|
"updated", // sort
|
||||||
|
-1, // limit
|
||||||
|
0, // offset
|
||||||
|
)
|
||||||
|
// log.Println("records", len(records))
|
||||||
|
if err != nil || len(records) == 0 {
|
||||||
|
// h.app.Logger().Error("Failed to query systems")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fiftySecondsAgo := time.Now().UTC().Add(-50 * time.Second)
|
||||||
|
batchSize := len(records)/4 + 1
|
||||||
|
done := 0
|
||||||
|
for _, record := range records {
|
||||||
|
// break if batch size reached or if the system was updated less than 50 seconds ago
|
||||||
|
if done >= batchSize || record.GetDateTime("updated").Time().After(fiftySecondsAgo) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
// don't increment for down systems to avoid them jamming the queue
|
||||||
|
// because they're always first when sorted by least recently updated
|
||||||
|
if record.GetString("status") != "down" {
|
||||||
|
done++
|
||||||
|
}
|
||||||
|
go h.updateSystem(record)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Hub) updateSystem(record *models.Record) {
|
||||||
|
var client *ssh.Client
|
||||||
|
var err error
|
||||||
|
|
||||||
|
// check if system connection data exists
|
||||||
|
if _, ok := h.systemConnections[record.Id]; ok {
|
||||||
|
client = h.systemConnections[record.Id]
|
||||||
|
} else {
|
||||||
|
// create system connection
|
||||||
|
client, err = h.createSystemConnection(record)
|
||||||
|
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.updateSystemStatus(record, "down")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
h.connectionLock.Lock()
|
||||||
|
h.systemConnections[record.Id] = client
|
||||||
|
h.connectionLock.Unlock()
|
||||||
|
}
|
||||||
|
// get system stats from agent
|
||||||
|
var systemData system.CombinedData
|
||||||
|
if err := requestJsonFromAgent(client, &systemData); err != nil {
|
||||||
|
if err.Error() == "bad client" {
|
||||||
|
// 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.deleteSystemConnection(record)
|
||||||
|
h.updateSystem(record)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
h.app.Logger().Error("Failed to get system stats: ", "err", err.Error())
|
||||||
|
h.updateSystemStatus(record, "down")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// update system record
|
||||||
|
record.Set("status", "up")
|
||||||
|
record.Set("info", systemData.Info)
|
||||||
|
if err := h.app.Dao().SaveRecord(record); err != nil {
|
||||||
|
h.app.Logger().Error("Failed to update record: ", "err", err.Error())
|
||||||
|
}
|
||||||
|
// add new system_stats record
|
||||||
|
system_stats, _ := h.app.Dao().FindCollectionByNameOrId("system_stats")
|
||||||
|
systemStatsRecord := models.NewRecord(system_stats)
|
||||||
|
systemStatsRecord.Set("system", record.Id)
|
||||||
|
systemStatsRecord.Set("stats", systemData.Stats)
|
||||||
|
systemStatsRecord.Set("type", "1m")
|
||||||
|
if err := h.app.Dao().SaveRecord(systemStatsRecord); err != nil {
|
||||||
|
h.app.Logger().Error("Failed to save record: ", "err", err.Error())
|
||||||
|
}
|
||||||
|
// add new container_stats record
|
||||||
|
if len(systemData.Containers) > 0 {
|
||||||
|
container_stats, _ := h.app.Dao().FindCollectionByNameOrId("container_stats")
|
||||||
|
containerStatsRecord := models.NewRecord(container_stats)
|
||||||
|
containerStatsRecord.Set("system", record.Id)
|
||||||
|
containerStatsRecord.Set("stats", systemData.Containers)
|
||||||
|
containerStatsRecord.Set("type", "1m")
|
||||||
|
if err := h.app.Dao().SaveRecord(containerStatsRecord); err != nil {
|
||||||
|
h.app.Logger().Error("Failed to save record: ", "err", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// set system to specified status and save record
|
||||||
|
func (h *Hub) updateSystemStatus(record *models.Record, status string) {
|
||||||
|
if record.GetString("status") != status {
|
||||||
|
record.Set("status", status)
|
||||||
|
if err := h.app.Dao().SaveRecord(record); err != nil {
|
||||||
|
h.app.Logger().Error("Failed to update record: ", "err", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Hub) deleteSystemConnection(record *models.Record) {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Hub) createSystemConnection(record *models.Record) (*ssh.Client, error) {
|
||||||
|
client, err := ssh.Dial("tcp", fmt.Sprintf("%s:%s", record.GetString("host"), record.GetString("port")), h.sshClientConfig)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return client, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Hub) createSSHClientConfig() error {
|
||||||
|
key, err := h.getSSHKey()
|
||||||
|
if err != nil {
|
||||||
|
h.app.Logger().Error("Failed to get SSH key: ", "err", err.Error())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the Signer for this private key.
|
||||||
|
signer, err := ssh.ParsePrivateKey(key)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
h.sshClientConfig = &ssh.ClientConfig{
|
||||||
|
User: "u",
|
||||||
|
Auth: []ssh.AuthMethod{
|
||||||
|
ssh.PublicKeys(signer),
|
||||||
|
},
|
||||||
|
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
|
||||||
|
Timeout: 5 * time.Second,
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func requestJsonFromAgent(client *ssh.Client, systemData *system.CombinedData) error {
|
||||||
|
session, err := newSessionWithTimeout(client, 5*time.Second)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("bad client")
|
||||||
|
}
|
||||||
|
defer session.Close()
|
||||||
|
|
||||||
|
stdout, err := session.StdoutPipe()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := session.Shell(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.NewDecoder(stdout).Decode(systemData); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// wait for the session to complete
|
||||||
|
if err := session.Wait(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
dataDir := h.app.DataDir()
|
||||||
|
// check if the key pair already exists
|
||||||
|
existingKey, err := os.ReadFile(dataDir + "/id_ed25519")
|
||||||
|
if err == nil {
|
||||||
|
if pubKey, err := os.ReadFile(h.app.DataDir() + "/id_ed25519.pub"); err == nil {
|
||||||
|
h.pubKey = strings.TrimSuffix(string(pubKey), "\n")
|
||||||
|
}
|
||||||
|
// return existing private key
|
||||||
|
return existingKey, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate the Ed25519 key pair
|
||||||
|
pubKey, privKey, err := ed25519.GenerateKey(nil)
|
||||||
|
if err != nil {
|
||||||
|
// h.app.Logger().Error("Error generating key pair:", "err", err.Error())
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the private key in OpenSSH format
|
||||||
|
privKeyBytes, err := ssh.MarshalPrivateKey(privKey, "")
|
||||||
|
if err != nil {
|
||||||
|
// h.app.Logger().Error("Error marshaling private key:", "err", err.Error())
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save the private key to a file
|
||||||
|
privateFile, err := os.Create(dataDir + "/id_ed25519")
|
||||||
|
if err != nil {
|
||||||
|
// h.app.Logger().Error("Error creating private key file:", "err", err.Error())
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer privateFile.Close()
|
||||||
|
|
||||||
|
if err := pem.Encode(privateFile, privKeyBytes); err != nil {
|
||||||
|
// h.app.Logger().Error("Error writing private key to file:", "err", err.Error())
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate the public key in OpenSSH format
|
||||||
|
publicKey, err := ssh.NewPublicKey(pubKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
pubKeyBytes := ssh.MarshalAuthorizedKey(publicKey)
|
||||||
|
h.pubKey = strings.TrimSuffix(string(pubKeyBytes), "\n")
|
||||||
|
|
||||||
|
// Save the public key to a file
|
||||||
|
publicFile, err := os.Create(dataDir + "/id_ed25519.pub")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer publicFile.Close()
|
||||||
|
|
||||||
|
if _, err := publicFile.Write(pubKeyBytes); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
h.app.Logger().Info("ed25519 SSH key pair generated successfully.")
|
||||||
|
h.app.Logger().Info("Private key saved to: " + dataDir + "/id_ed25519")
|
||||||
|
h.app.Logger().Info("Public key saved to: " + dataDir + "/id_ed25519.pub")
|
||||||
|
|
||||||
|
existingKey, err = os.ReadFile(dataDir + "/id_ed25519")
|
||||||
|
if err == nil {
|
||||||
|
return existingKey, nil
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
317
beszel/internal/records/records.go
Normal file
@@ -0,0 +1,317 @@
|
|||||||
|
// Package records handles creating longer records and deleting old records.
|
||||||
|
package records
|
||||||
|
|
||||||
|
import (
|
||||||
|
"beszel/internal/entities/container"
|
||||||
|
"beszel/internal/entities/system"
|
||||||
|
"log"
|
||||||
|
"math"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/pocketbase/dbx"
|
||||||
|
"github.com/pocketbase/pocketbase"
|
||||||
|
"github.com/pocketbase/pocketbase/daos"
|
||||||
|
"github.com/pocketbase/pocketbase/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
type RecordManager struct {
|
||||||
|
app *pocketbase.PocketBase
|
||||||
|
}
|
||||||
|
|
||||||
|
type LongerRecordData struct {
|
||||||
|
shorterType string
|
||||||
|
longerType string
|
||||||
|
longerTimeDuration time.Duration
|
||||||
|
expectedShorterRecords int
|
||||||
|
}
|
||||||
|
|
||||||
|
type RecordDeletionData struct {
|
||||||
|
recordType string
|
||||||
|
retention time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRecordManager(app *pocketbase.PocketBase) *RecordManager {
|
||||||
|
return &RecordManager{app}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create longer records by averaging shorter records
|
||||||
|
func (rm *RecordManager) CreateLongerRecords() {
|
||||||
|
// start := time.Now()
|
||||||
|
recordData := []LongerRecordData{
|
||||||
|
{
|
||||||
|
shorterType: "1m",
|
||||||
|
expectedShorterRecords: 10,
|
||||||
|
longerType: "10m",
|
||||||
|
longerTimeDuration: -10 * time.Minute,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
shorterType: "10m",
|
||||||
|
expectedShorterRecords: 2,
|
||||||
|
longerType: "20m",
|
||||||
|
longerTimeDuration: -20 * time.Minute,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
shorterType: "20m",
|
||||||
|
expectedShorterRecords: 6,
|
||||||
|
longerType: "120m",
|
||||||
|
longerTimeDuration: -120 * time.Minute,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
shorterType: "120m",
|
||||||
|
expectedShorterRecords: 4,
|
||||||
|
longerType: "480m",
|
||||||
|
longerTimeDuration: -480 * time.Minute,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
// wrap the operations in a transaction
|
||||||
|
rm.app.Dao().RunInTransaction(func(txDao *daos.Dao) error {
|
||||||
|
activeSystems, err := txDao.FindRecordsByExpr("systems", dbx.NewExp("status = 'up'"))
|
||||||
|
if err != nil {
|
||||||
|
log.Println("failed to get active systems", "err", err.Error())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
collections := map[string]*models.Collection{}
|
||||||
|
for _, collectionName := range []string{"system_stats", "container_stats"} {
|
||||||
|
collection, _ := txDao.FindCollectionByNameOrId(collectionName)
|
||||||
|
collections[collectionName] = collection
|
||||||
|
}
|
||||||
|
|
||||||
|
// loop through all active systems, time periods, and collections
|
||||||
|
for _, system := range activeSystems {
|
||||||
|
// log.Println("processing system", system.GetString("name"))
|
||||||
|
for _, recordData := range recordData {
|
||||||
|
// log.Println("processing longer record type", recordData.longerType)
|
||||||
|
// add one minute padding for longer records because they are created slightly later than the job start time
|
||||||
|
longerRecordPeriod := time.Now().UTC().Add(recordData.longerTimeDuration + time.Minute)
|
||||||
|
// shorter records are created independently of longer records, so we shouldn't need to add padding
|
||||||
|
shorterRecordPeriod := time.Now().UTC().Add(recordData.longerTimeDuration)
|
||||||
|
// loop through both collections
|
||||||
|
for _, collection := range collections {
|
||||||
|
// check creation time of last longer record if not 10m, since 10m is created every run
|
||||||
|
if recordData.longerType != "10m" {
|
||||||
|
lastLongerRecord, err := txDao.FindFirstRecordByFilter(
|
||||||
|
collection.Id,
|
||||||
|
"type = {:type} && system = {:system} && created > {:created}",
|
||||||
|
dbx.Params{"type": recordData.longerType, "system": system.Id, "created": longerRecordPeriod},
|
||||||
|
)
|
||||||
|
// continue if longer record exists
|
||||||
|
if err == nil || lastLongerRecord != nil {
|
||||||
|
// log.Println("longer record found. continuing")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// get shorter records from the past x minutes
|
||||||
|
allShorterRecords, err := txDao.FindRecordsByExpr(
|
||||||
|
collection.Id,
|
||||||
|
dbx.NewExp(
|
||||||
|
"type = {:type} AND system = {:system} AND created > {:created}",
|
||||||
|
dbx.Params{"type": recordData.shorterType, "system": system.Id, "created": shorterRecordPeriod},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
// continue if not enough shorter records
|
||||||
|
if err != nil || len(allShorterRecords) < recordData.expectedShorterRecords {
|
||||||
|
// log.Println("not enough shorter records. continue.", len(allShorterRecords), recordData.expectedShorterRecords)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// average the shorter records and create longer record
|
||||||
|
var stats interface{}
|
||||||
|
switch collection.Name {
|
||||||
|
case "system_stats":
|
||||||
|
stats = rm.AverageSystemStats(allShorterRecords)
|
||||||
|
case "container_stats":
|
||||||
|
stats = rm.AverageContainerStats(allShorterRecords)
|
||||||
|
}
|
||||||
|
longerRecord := models.NewRecord(collection)
|
||||||
|
longerRecord.Set("system", system.Id)
|
||||||
|
longerRecord.Set("stats", stats)
|
||||||
|
longerRecord.Set("type", recordData.longerType)
|
||||||
|
if err := txDao.SaveRecord(longerRecord); err != nil {
|
||||||
|
log.Println("failed to save longer record", "err", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
// log.Println("finished creating longer records", "time (ms)", time.Since(start).Milliseconds())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate the average stats of a list of system_stats records without reflect
|
||||||
|
func (rm *RecordManager) AverageSystemStats(records []*models.Record) system.Stats {
|
||||||
|
var sum system.Stats
|
||||||
|
sum.Temperatures = make(map[string]float64)
|
||||||
|
sum.ExtraFs = make(map[string]*system.FsStats)
|
||||||
|
|
||||||
|
count := float64(len(records))
|
||||||
|
// use different counter for temps in case some records don't have them
|
||||||
|
tempCount := float64(0)
|
||||||
|
|
||||||
|
var stats system.Stats
|
||||||
|
for _, record := range records {
|
||||||
|
record.UnmarshalJSONField("stats", &stats)
|
||||||
|
sum.Cpu += stats.Cpu
|
||||||
|
sum.Mem += stats.Mem
|
||||||
|
sum.MemUsed += stats.MemUsed
|
||||||
|
sum.MemPct += stats.MemPct
|
||||||
|
sum.MemBuffCache += stats.MemBuffCache
|
||||||
|
sum.Swap += stats.Swap
|
||||||
|
sum.SwapUsed += stats.SwapUsed
|
||||||
|
sum.DiskTotal += stats.DiskTotal
|
||||||
|
sum.DiskUsed += stats.DiskUsed
|
||||||
|
sum.DiskPct += stats.DiskPct
|
||||||
|
sum.DiskReadPs += stats.DiskReadPs
|
||||||
|
sum.DiskWritePs += stats.DiskWritePs
|
||||||
|
sum.NetworkSent += stats.NetworkSent
|
||||||
|
sum.NetworkRecv += stats.NetworkRecv
|
||||||
|
// add temps to sum
|
||||||
|
if stats.Temperatures != nil {
|
||||||
|
tempCount++
|
||||||
|
for key, value := range stats.Temperatures {
|
||||||
|
if _, ok := sum.Temperatures[key]; !ok {
|
||||||
|
sum.Temperatures[key] = 0
|
||||||
|
}
|
||||||
|
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{
|
||||||
|
Cpu: twoDecimals(sum.Cpu / count),
|
||||||
|
Mem: twoDecimals(sum.Mem / count),
|
||||||
|
MemUsed: twoDecimals(sum.MemUsed / count),
|
||||||
|
MemPct: twoDecimals(sum.MemPct / count),
|
||||||
|
MemBuffCache: twoDecimals(sum.MemBuffCache / count),
|
||||||
|
Swap: twoDecimals(sum.Swap / count),
|
||||||
|
SwapUsed: twoDecimals(sum.SwapUsed / count),
|
||||||
|
DiskTotal: twoDecimals(sum.DiskTotal / count),
|
||||||
|
DiskUsed: twoDecimals(sum.DiskUsed / count),
|
||||||
|
DiskPct: twoDecimals(sum.DiskPct / count),
|
||||||
|
DiskReadPs: twoDecimals(sum.DiskReadPs / count),
|
||||||
|
DiskWritePs: twoDecimals(sum.DiskWritePs / count),
|
||||||
|
NetworkSent: twoDecimals(sum.NetworkSent / count),
|
||||||
|
NetworkRecv: twoDecimals(sum.NetworkRecv / count),
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(sum.Temperatures) != 0 {
|
||||||
|
stats.Temperatures = make(map[string]float64)
|
||||||
|
for key, value := range sum.Temperatures {
|
||||||
|
stats.Temperatures[key] = twoDecimals(value / tempCount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate the average stats of a list of container_stats records
|
||||||
|
func (rm *RecordManager) AverageContainerStats(records []*models.Record) (stats []container.Stats) {
|
||||||
|
sums := make(map[string]*container.Stats)
|
||||||
|
count := float64(len(records))
|
||||||
|
|
||||||
|
var containerStats []container.Stats
|
||||||
|
for _, record := range records {
|
||||||
|
record.UnmarshalJSONField("stats", &containerStats)
|
||||||
|
for _, stat := range containerStats {
|
||||||
|
if _, ok := sums[stat.Name]; !ok {
|
||||||
|
sums[stat.Name] = &container.Stats{Name: stat.Name, Cpu: 0, Mem: 0}
|
||||||
|
}
|
||||||
|
sums[stat.Name].Cpu += stat.Cpu
|
||||||
|
sums[stat.Name].Mem += stat.Mem
|
||||||
|
sums[stat.Name].NetworkSent += stat.NetworkSent
|
||||||
|
sums[stat.Name].NetworkRecv += stat.NetworkRecv
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, value := range sums {
|
||||||
|
stats = append(stats, container.Stats{
|
||||||
|
Name: value.Name,
|
||||||
|
Cpu: twoDecimals(value.Cpu / count),
|
||||||
|
Mem: twoDecimals(value.Mem / count),
|
||||||
|
NetworkSent: twoDecimals(value.NetworkSent / count),
|
||||||
|
NetworkRecv: twoDecimals(value.NetworkRecv / count),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return stats
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rm *RecordManager) DeleteOldRecords() {
|
||||||
|
// start := time.Now()
|
||||||
|
collections := []string{"system_stats", "container_stats"}
|
||||||
|
recordData := []RecordDeletionData{
|
||||||
|
{
|
||||||
|
recordType: "1m",
|
||||||
|
retention: time.Hour,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
recordType: "10m",
|
||||||
|
retention: 12 * time.Hour,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
recordType: "20m",
|
||||||
|
retention: 24 * time.Hour,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
recordType: "120m",
|
||||||
|
retention: 7 * 24 * time.Hour,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
recordType: "480m",
|
||||||
|
retention: 30 * 24 * time.Hour,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
rm.app.Dao().RunInTransaction(func(txDao *daos.Dao) error {
|
||||||
|
for _, recordData := range recordData {
|
||||||
|
exp := dbx.NewExp(
|
||||||
|
"type = {:type} AND created < {:created}",
|
||||||
|
dbx.Params{"type": recordData.recordType, "created": time.Now().UTC().Add(-recordData.retention)},
|
||||||
|
)
|
||||||
|
for _, collectionSlug := range collections {
|
||||||
|
collectionRecords, err := txDao.FindRecordsByExpr(collectionSlug, exp)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, record := range collectionRecords {
|
||||||
|
err := txDao.DeleteRecord(record)
|
||||||
|
if err != nil {
|
||||||
|
rm.app.Logger().Error("Failed to delete records", "err", err.Error())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
// log.Println("finished deleting old records", "time (ms)", time.Since(start).Milliseconds())
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Round float to two decimals */
|
||||||
|
func twoDecimals(value float64) float64 {
|
||||||
|
return math.Round(value*100) / 100
|
||||||
|
}
|
||||||
99
beszel/internal/update/update.go
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
package update
|
||||||
|
|
||||||
|
import (
|
||||||
|
"beszel"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/blang/semver"
|
||||||
|
"github.com/rhysd/go-github-selfupdate/selfupdate"
|
||||||
|
)
|
||||||
|
|
||||||
|
func UpdateBeszel() {
|
||||||
|
var latest *selfupdate.Release
|
||||||
|
var found bool
|
||||||
|
var err error
|
||||||
|
currentVersion := semver.MustParse(beszel.Version)
|
||||||
|
fmt.Println("beszel", currentVersion)
|
||||||
|
fmt.Println("Checking for updates...")
|
||||||
|
updater, _ := selfupdate.NewUpdater(selfupdate.Config{
|
||||||
|
Filters: []string{"beszel_"},
|
||||||
|
})
|
||||||
|
latest, found, err = updater.DetectLatest("henrygd/beszel")
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Error checking for updates:", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !found {
|
||||||
|
fmt.Println("No updates found")
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("Latest version:", latest.Version)
|
||||||
|
|
||||||
|
if latest.Version.LTE(currentVersion) {
|
||||||
|
fmt.Println("You are up to date")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var binaryPath string
|
||||||
|
fmt.Printf("Updating from %s to %s...\n", currentVersion, latest.Version)
|
||||||
|
binaryPath, err = os.Executable()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Error getting binary path:", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
err = selfupdate.UpdateTo(latest.AssetURL, binaryPath)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Please try rerunning with sudo. Error:", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
fmt.Printf("Successfully updated to %s\n\n%s\n", latest.Version, strings.TrimSpace(latest.ReleaseNotes))
|
||||||
|
}
|
||||||
|
|
||||||
|
func UpdateBeszelAgent() {
|
||||||
|
var latest *selfupdate.Release
|
||||||
|
var found bool
|
||||||
|
var err error
|
||||||
|
currentVersion := semver.MustParse(beszel.Version)
|
||||||
|
fmt.Println("beszel-agent", currentVersion)
|
||||||
|
fmt.Println("Checking for updates...")
|
||||||
|
updater, _ := selfupdate.NewUpdater(selfupdate.Config{
|
||||||
|
Filters: []string{"beszel-agent"},
|
||||||
|
})
|
||||||
|
latest, found, err = updater.DetectLatest("henrygd/beszel")
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Error checking for updates:", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !found {
|
||||||
|
fmt.Println("No updates found")
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("Latest version:", latest.Version)
|
||||||
|
|
||||||
|
if latest.Version.LTE(currentVersion) {
|
||||||
|
fmt.Println("You are up to date")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var binaryPath string
|
||||||
|
fmt.Printf("Updating from %s to %s...\n", currentVersion, latest.Version)
|
||||||
|
binaryPath, err = os.Executable()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Error getting binary path:", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
err = selfupdate.UpdateTo(latest.AssetURL, binaryPath)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Please try rerunning with sudo. Error:", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
fmt.Printf("Successfully updated to %s\n\n%s\n", latest.Version, strings.TrimSpace(latest.ReleaseNotes))
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
@@ -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
|
Before Width: | Height: | Size: 378 B After Width: | Height: | Size: 378 B |
|
Before Width: | Height: | Size: 196 B After Width: | Height: | Size: 196 B |
|
Before Width: | Height: | Size: 506 B After Width: | Height: | Size: 506 B |
|
Before Width: | Height: | Size: 295 B After Width: | Height: | Size: 295 B |
|
Before Width: | Height: | Size: 906 B After Width: | Height: | Size: 906 B |
|
Before Width: | Height: | Size: 906 B After Width: | Height: | Size: 906 B |
|
Before Width: | Height: | Size: 903 B After Width: | Height: | Size: 903 B |
|
Before Width: | Height: | Size: 907 B After Width: | Height: | Size: 907 B |
|
Before Width: | Height: | Size: 406 B After Width: | Height: | Size: 406 B |
|
Before Width: | Height: | Size: 470 B After Width: | Height: | Size: 470 B |
|
Before Width: | Height: | Size: 302 B After Width: | Height: | Size: 302 B |
|
Before Width: | Height: | Size: 299 B After Width: | Height: | Size: 299 B |
|
Before Width: | Height: | Size: 856 B After Width: | Height: | Size: 856 B |
|
Before Width: | Height: | Size: 257 B After Width: | Height: | Size: 257 B |
|
Before Width: | Height: | Size: 276 B After Width: | Height: | Size: 276 B |
|
Before Width: | Height: | Size: 227 B After Width: | Height: | Size: 227 B |
|
Before Width: | Height: | Size: 495 B After Width: | Height: | Size: 495 B |
|
Before Width: | Height: | Size: 154 B After Width: | Height: | Size: 154 B |
|
Before Width: | Height: | Size: 206 B After Width: | Height: | Size: 206 B |
|
Before Width: | Height: | Size: 371 B After Width: | Height: | Size: 371 B |
@@ -13,12 +13,13 @@ import { TooltipProvider, Tooltip, TooltipTrigger, TooltipContent } from '@/comp
|
|||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
import { Label } from '@/components/ui/label'
|
import { Label } from '@/components/ui/label'
|
||||||
import { $publicKey, pb } from '@/lib/stores'
|
import { $publicKey, pb } from '@/lib/stores'
|
||||||
import { Copy, Plus } from 'lucide-react'
|
import { Copy, PlusIcon } from 'lucide-react'
|
||||||
import { useState, useRef, MutableRefObject, useEffect } from 'react'
|
import { useState, useRef, MutableRefObject } from 'react'
|
||||||
import { useStore } from '@nanostores/react'
|
import { useStore } from '@nanostores/react'
|
||||||
import { copyToClipboard } from '@/lib/utils'
|
import { cn, copyToClipboard, isReadOnlyUser } from '@/lib/utils'
|
||||||
|
import { navigate } from './router'
|
||||||
|
|
||||||
export function AddSystemButton() {
|
export function AddSystemButton({ className }: { className?: string }) {
|
||||||
const [open, setOpen] = useState(false)
|
const [open, setOpen] = useState(false)
|
||||||
const port = useRef() as MutableRefObject<HTMLInputElement>
|
const port = useRef() as MutableRefObject<HTMLInputElement>
|
||||||
const publicKey = useStore($publicKey)
|
const publicKey = useStore($publicKey)
|
||||||
@@ -32,22 +33,14 @@ export function AddSystemButton() {
|
|||||||
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`)
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (publicKey || !open) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// get public key
|
|
||||||
pb.send('/api/beszel/getkey', {}).then(({ key }) => {
|
|
||||||
$publicKey.set(key)
|
|
||||||
})
|
|
||||||
}, [open])
|
|
||||||
|
|
||||||
async function handleSubmit(e: SubmitEvent) {
|
async function handleSubmit(e: SubmitEvent) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
const formData = new FormData(e.target as HTMLFormElement)
|
const formData = new FormData(e.target as HTMLFormElement)
|
||||||
@@ -56,6 +49,7 @@ export function AddSystemButton() {
|
|||||||
try {
|
try {
|
||||||
setOpen(false)
|
setOpen(false)
|
||||||
await pb.collection('systems').create(data)
|
await pb.collection('systems').create(data)
|
||||||
|
navigate('/')
|
||||||
// console.log(record)
|
// console.log(record)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log(e)
|
console.log(e)
|
||||||
@@ -65,9 +59,12 @@ export function AddSystemButton() {
|
|||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={setOpen}>
|
<Dialog open={open} onOpenChange={setOpen}>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button variant="outline" className="flex gap-1">
|
<Button
|
||||||
<Plus className="h-4 w-4 mr-auto" />
|
variant="outline"
|
||||||
Add <span className="hidden sm:inline">System</span>
|
className={cn('flex gap-1 max-xs:h-[2.4rem]', className, isReadOnlyUser() && 'hidden')}
|
||||||
|
>
|
||||||
|
<PlusIcon className="h-4 w-4 -ml-1" />
|
||||||
|
Add <span className="hidden xs:inline">System</span>
|
||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="w-[90%] sm:max-w-[425px] rounded-lg">
|
<DialogContent className="w-[90%] sm:max-w-[425px] rounded-lg">
|
||||||
@@ -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"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
@@ -83,7 +84,7 @@ export default function BandwidthChart({
|
|||||||
name="Sent"
|
name="Sent"
|
||||||
type="monotoneX"
|
type="monotoneX"
|
||||||
fill="hsl(var(--chart-5))"
|
fill="hsl(var(--chart-5))"
|
||||||
fillOpacity={0.4}
|
fillOpacity={0.2}
|
||||||
stroke="hsl(var(--chart-5))"
|
stroke="hsl(var(--chart-5))"
|
||||||
// animationDuration={1200}
|
// animationDuration={1200}
|
||||||
isAnimationActive={false}
|
isAnimationActive={false}
|
||||||
@@ -93,7 +94,7 @@ export default function BandwidthChart({
|
|||||||
name="Received"
|
name="Received"
|
||||||
type="monotoneX"
|
type="monotoneX"
|
||||||
fill="hsl(var(--chart-2))"
|
fill="hsl(var(--chart-2))"
|
||||||
fillOpacity={0.4}
|
fillOpacity={0.2}
|
||||||
stroke="hsl(var(--chart-2))"
|
stroke="hsl(var(--chart-2))"
|
||||||
// animationDuration={1200}
|
// animationDuration={1200}
|
||||||
isAnimationActive={false}
|
isAnimationActive={false}
|
||||||
@@ -9,6 +9,7 @@ import { $chartTime } from '@/lib/stores'
|
|||||||
import { chartTimeData, cn } from '@/lib/utils'
|
import { chartTimeData, cn } from '@/lib/utils'
|
||||||
import { ChartTimes } from '@/types'
|
import { ChartTimes } from '@/types'
|
||||||
import { useStore } from '@nanostores/react'
|
import { useStore } from '@nanostores/react'
|
||||||
|
import { HistoryIcon } from 'lucide-react'
|
||||||
|
|
||||||
export default function ChartTimeSelect({ className }: { className?: string }) {
|
export default function ChartTimeSelect({ className }: { className?: string }) {
|
||||||
const chartTime = useStore($chartTime)
|
const chartTime = useStore($chartTime)
|
||||||
@@ -19,7 +20,8 @@ export default function ChartTimeSelect({ className }: { className?: string }) {
|
|||||||
value={chartTime}
|
value={chartTime}
|
||||||
onValueChange={(value: ChartTimes) => $chartTime.set(value)}
|
onValueChange={(value: ChartTimes) => $chartTime.set(value)}
|
||||||
>
|
>
|
||||||
<SelectTrigger className={cn(className, 'px-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-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])
|
||||||
|
|
||||||
@@ -74,6 +75,7 @@ export default function ContainerCpuChart({
|
|||||||
>
|
>
|
||||||
<AreaChart
|
<AreaChart
|
||||||
accessibilityLayer
|
accessibilityLayer
|
||||||
|
// syncId={'cpu'}
|
||||||
data={chartData}
|
data={chartData}
|
||||||
margin={{
|
margin={{
|
||||||
top: 10,
|
top: 10,
|
||||||
@@ -108,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) => {
|
||||||
<Area
|
const filtered = filter && !key.includes(filter)
|
||||||
key={key}
|
let fillOpacity = filtered ? 0.05 : 0.4
|
||||||
// isAnimationActive={chartData.length < 20}
|
let strokeOpacity = filtered ? 0.1 : 1
|
||||||
isAnimationActive={false}
|
return (
|
||||||
// animateNewValues={false}
|
<Area
|
||||||
// animationDuration={1200}
|
key={key}
|
||||||
dataKey={key}
|
isAnimationActive={false}
|
||||||
type="monotoneX"
|
dataKey={key}
|
||||||
fill={chartConfig[key].color}
|
type="monotoneX"
|
||||||
fillOpacity={0.4}
|
fill={chartConfig[key].color}
|
||||||
stroke={chartConfig[key].color}
|
fillOpacity={fillOpacity}
|
||||||
stackId="a"
|
stroke={chartConfig[key].color}
|
||||||
/>
|
strokeOpacity={strokeOpacity}
|
||||||
))}
|
activeDot={{ opacity: filtered ? 0 : 1 }}
|
||||||
|
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) => {
|
||||||
<Area
|
const filtered = filter && !key.includes(filter)
|
||||||
key={key}
|
let fillOpacity = filtered ? 0.05 : 0.4
|
||||||
// animationDuration={1200}
|
let strokeOpacity = filtered ? 0.1 : 1
|
||||||
isAnimationActive={false}
|
return (
|
||||||
dataKey={key}
|
<Area
|
||||||
type="monotoneX"
|
key={key}
|
||||||
fill={chartConfig[key].color}
|
isAnimationActive={false}
|
||||||
fillOpacity={0.4}
|
dataKey={key}
|
||||||
stroke={chartConfig[key].color}
|
type="monotoneX"
|
||||||
stackId="a"
|
fill={chartConfig[key].color}
|
||||||
/>
|
strokeOpacity={strokeOpacity}
|
||||||
))}
|
fillOpacity={fillOpacity}
|
||||||
|
stroke={chartConfig[key].color}
|
||||||
|
activeDot={filtered ? false : {}}
|
||||||
|
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,20 +139,27 @@ export default function ContainerCpuChart({
|
|||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
{Object.keys(chartConfig).map((key) => (
|
{Object.keys(chartConfig).map((key) => {
|
||||||
<Area
|
const filtered = filter && !key.includes(filter)
|
||||||
key={key}
|
let fillOpacity = filtered ? 0.05 : 0.4
|
||||||
name={key}
|
let strokeOpacity = filtered ? 0.1 : 1
|
||||||
// animationDuration={1200}
|
return (
|
||||||
isAnimationActive={false}
|
<Area
|
||||||
dataKey={(data) => data?.[key]?.[2] ?? 0}
|
key={key}
|
||||||
type="monotoneX"
|
name={key}
|
||||||
fill={chartConfig[key].color}
|
// animationDuration={1200}
|
||||||
fillOpacity={0.4}
|
isAnimationActive={false}
|
||||||
stroke={chartConfig[key].color}
|
dataKey={(data) => data?.[key]?.[2] ?? 0}
|
||||||
stackId="a"
|
type="monotoneX"
|
||||||
/>
|
fill={chartConfig[key].color}
|
||||||
))}
|
fillOpacity={fillOpacity}
|
||||||
|
stroke={chartConfig[key].color}
|
||||||
|
strokeOpacity={strokeOpacity}
|
||||||
|
activeDot={{ opacity: filtered ? 0 : 1 }}
|
||||||
|
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'
|
||||||
@@ -29,7 +29,12 @@ export default function CpuChart({
|
|||||||
'opacity-100': yAxisSet,
|
'opacity-100': yAxisSet,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<AreaChart accessibilityLayer data={systemData} margin={{ top: 10 }}>
|
<AreaChart
|
||||||
|
accessibilityLayer
|
||||||
|
data={systemData}
|
||||||
|
margin={{ top: 10 }}
|
||||||
|
// syncId={'cpu'}
|
||||||
|
>
|
||||||
<CartesianGrid vertical={false} />
|
<CartesianGrid vertical={false} />
|
||||||
<YAxis
|
<YAxis
|
||||||
className="tracking-tighter"
|
className="tracking-tighter"
|
||||||
@@ -55,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"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
@@ -87,7 +88,7 @@ export default function DiskIoChart({
|
|||||||
name="Write"
|
name="Write"
|
||||||
type="monotoneX"
|
type="monotoneX"
|
||||||
fill="hsl(var(--chart-3))"
|
fill="hsl(var(--chart-3))"
|
||||||
fillOpacity={0.4}
|
fillOpacity={0.3}
|
||||||
stroke="hsl(var(--chart-3))"
|
stroke="hsl(var(--chart-3))"
|
||||||
// animationDuration={1200}
|
// animationDuration={1200}
|
||||||
isAnimationActive={false}
|
isAnimationActive={false}
|
||||||
@@ -97,7 +98,7 @@ export default function DiskIoChart({
|
|||||||
name="Read"
|
name="Read"
|
||||||
type="monotoneX"
|
type="monotoneX"
|
||||||
fill="hsl(var(--chart-1))"
|
fill="hsl(var(--chart-1))"
|
||||||
fillOpacity={0.4}
|
fillOpacity={0.3}
|
||||||
stroke="hsl(var(--chart-1))"
|
stroke="hsl(var(--chart-1))"
|
||||||
// animationDuration={1200}
|
// animationDuration={1200}
|
||||||
isAnimationActive={false}
|
isAnimationActive={false}
|
||||||
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
@@ -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'
|
||||||
@@ -46,6 +53,7 @@ export default function MemChart({
|
|||||||
<YAxis
|
<YAxis
|
||||||
// use "ticks" instead of domain / tickcount if need more control
|
// use "ticks" instead of domain / tickcount if need more control
|
||||||
domain={[0, totalMem]}
|
domain={[0, totalMem]}
|
||||||
|
tickCount={9}
|
||||||
className="tracking-tighter"
|
className="tracking-tighter"
|
||||||
width={yAxisWidth}
|
width={yAxisWidth}
|
||||||
tickLine={false}
|
tickLine={false}
|
||||||
@@ -70,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"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
129
beszel/site/src/components/charts/temperature-chart.tsx
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
import { CartesianGrid, Line, LineChart, XAxis, YAxis } from 'recharts'
|
||||||
|
|
||||||
|
import {
|
||||||
|
ChartContainer,
|
||||||
|
ChartLegend,
|
||||||
|
ChartLegendContent,
|
||||||
|
ChartTooltip,
|
||||||
|
ChartTooltipContent,
|
||||||
|
} from '@/components/ui/chart'
|
||||||
|
import {
|
||||||
|
chartTimeData,
|
||||||
|
cn,
|
||||||
|
formatShortDate,
|
||||||
|
toFixedWithoutTrailingZeros,
|
||||||
|
twoDecimalString,
|
||||||
|
useYaxisWidth,
|
||||||
|
} from '@/lib/utils'
|
||||||
|
import { useStore } from '@nanostores/react'
|
||||||
|
import { $chartTime } from '@/lib/stores'
|
||||||
|
import { SystemStatsRecord } from '@/types'
|
||||||
|
import { useMemo, useRef } from 'react'
|
||||||
|
|
||||||
|
export default function TemperatureChart({
|
||||||
|
ticks,
|
||||||
|
systemData,
|
||||||
|
}: {
|
||||||
|
ticks: number[]
|
||||||
|
systemData: SystemStatsRecord[]
|
||||||
|
}) {
|
||||||
|
const chartRef = useRef<HTMLDivElement>(null)
|
||||||
|
const yAxisWidth = useYaxisWidth(chartRef)
|
||||||
|
const chartTime = useStore($chartTime)
|
||||||
|
|
||||||
|
/** Format temperature data for chart and assign colors */
|
||||||
|
const newChartData = useMemo(() => {
|
||||||
|
const chartData = { data: [], colors: {} } as {
|
||||||
|
data: Record<string, number | string>[]
|
||||||
|
colors: Record<string, string>
|
||||||
|
}
|
||||||
|
const tempSums = {} as Record<string, number>
|
||||||
|
for (let data of systemData) {
|
||||||
|
let newData = { created: data.created } as Record<string, number | string>
|
||||||
|
let keys = Object.keys(data.stats?.t ?? {})
|
||||||
|
for (let i = 0; i < keys.length; i++) {
|
||||||
|
let key = keys[i]
|
||||||
|
newData[key] = data.stats.t![key]
|
||||||
|
tempSums[key] = (tempSums[key] ?? 0) + newData[key]
|
||||||
|
}
|
||||||
|
chartData.data.push(newData)
|
||||||
|
}
|
||||||
|
const keys = Object.keys(tempSums).sort((a, b) => tempSums[b] - tempSums[a])
|
||||||
|
for (let key of keys) {
|
||||||
|
chartData.colors[key] = `hsl(${((keys.indexOf(key) * 360) / keys.length) % 360}, 60%, 55%)`
|
||||||
|
}
|
||||||
|
return chartData
|
||||||
|
}, [systemData])
|
||||||
|
|
||||||
|
const yAxisSet = useMemo(() => yAxisWidth !== 180, [yAxisWidth])
|
||||||
|
|
||||||
|
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,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<LineChart
|
||||||
|
accessibilityLayer
|
||||||
|
data={newChartData.data}
|
||||||
|
margin={{
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
top: 10,
|
||||||
|
bottom: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CartesianGrid vertical={false} />
|
||||||
|
<YAxis
|
||||||
|
className="tracking-tighter"
|
||||||
|
width={yAxisWidth}
|
||||||
|
tickFormatter={(value) => toFixedWithoutTrailingZeros(value, 2)}
|
||||||
|
tickLine={false}
|
||||||
|
axisLine={false}
|
||||||
|
unit={' °C'}
|
||||||
|
/>
|
||||||
|
<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}
|
||||||
|
// @ts-ignore
|
||||||
|
itemSorter={(a, b) => b.value - a.value}
|
||||||
|
content={
|
||||||
|
<ChartTooltipContent
|
||||||
|
labelFormatter={(_, data) => formatShortDate(data[0].payload.created)}
|
||||||
|
contentFormatter={(item) => twoDecimalString(item.value) + ' °C'}
|
||||||
|
indicator="line"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
{Object.keys(newChartData.colors).map((key) => (
|
||||||
|
<Line
|
||||||
|
key={key}
|
||||||
|
dataKey={key}
|
||||||
|
name={key}
|
||||||
|
type="monotoneX"
|
||||||
|
dot={false}
|
||||||
|
strokeWidth={1.5}
|
||||||
|
stroke={newChartData.colors[key]}
|
||||||
|
isAnimationActive={false}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
<ChartLegend content={<ChartLegendContent />} />
|
||||||
|
</LineChart>
|
||||||
|
</ChartContainer>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -43,7 +43,7 @@ export default function CommandPalette() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<CommandDialog open={open} onOpenChange={setOpen}>
|
<CommandDialog open={open} onOpenChange={setOpen}>
|
||||||
<CommandInput placeholder="Type a command or search..." />
|
<CommandInput placeholder="Search for systems or settings..." />
|
||||||
<CommandList>
|
<CommandList>
|
||||||
<CommandEmpty>No results found.</CommandEmpty>
|
<CommandEmpty>No results found.</CommandEmpty>
|
||||||
<CommandGroup heading="Suggestions">
|
<CommandGroup heading="Suggestions">
|
||||||
@@ -77,7 +77,7 @@ export default function CommandPalette() {
|
|||||||
<CommandItem
|
<CommandItem
|
||||||
key={system.id}
|
key={system.id}
|
||||||
onSelect={() => {
|
onSelect={() => {
|
||||||
navigate(`/system/${system.name}`)
|
navigate(`/system/${encodeURIComponent(system.name)}`)
|
||||||
setOpen(false)
|
setOpen(false)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
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>
|
||||||
)
|
)
|
||||||
85
beszel/site/src/components/routes/home.tsx
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
import { Suspense, lazy, useEffect, useState } from 'react'
|
||||||
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../ui/card'
|
||||||
|
import { $alerts, $hubVersion, $systems, pb } from '@/lib/stores'
|
||||||
|
import { useStore } from '@nanostores/react'
|
||||||
|
import { GithubIcon } from 'lucide-react'
|
||||||
|
import { Separator } from '../ui/separator'
|
||||||
|
import { updateRecordList, updateSystemList } from '@/lib/utils'
|
||||||
|
import { AlertRecord, SystemRecord } from '@/types'
|
||||||
|
import { Input } from '../ui/input'
|
||||||
|
|
||||||
|
const SystemsTable = lazy(() => import('../systems-table/systems-table'))
|
||||||
|
|
||||||
|
export default function () {
|
||||||
|
const hubVersion = useStore($hubVersion)
|
||||||
|
const [filter, setFilter] = useState<string>()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
document.title = 'Dashboard / Beszel'
|
||||||
|
|
||||||
|
// make sure we have the latest list of systems
|
||||||
|
updateSystemList()
|
||||||
|
|
||||||
|
// subscribe to real time updates for systems / alerts
|
||||||
|
pb.collection<SystemRecord>('systems').subscribe('*', (e) => {
|
||||||
|
updateRecordList(e, $systems)
|
||||||
|
})
|
||||||
|
pb.collection<AlertRecord>('alerts').subscribe('*', (e) => {
|
||||||
|
updateRecordList(e, $alerts)
|
||||||
|
})
|
||||||
|
return () => {
|
||||||
|
pb.collection('systems').unsubscribe('*')
|
||||||
|
pb.collection('alerts').unsubscribe('*')
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="pb-5 px-2 sm:px-6 max-sm:pt-5 max-sm:pb-1">
|
||||||
|
<div className="grid md:flex gap-3 w-full items-end">
|
||||||
|
<div className="px-2 sm:px-1">
|
||||||
|
<CardTitle className="mb-2.5">All Systems</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Updated in real time. Press{' '}
|
||||||
|
<kbd className="pointer-events-none inline-flex h-5 select-none items-center gap-0.5 rounded border bg-muted px-1.5 font-mono text-[10px] font-medium text-muted-foreground opacity-100">
|
||||||
|
<span className="text-xs">⌘</span>K
|
||||||
|
</kbd>{' '}
|
||||||
|
to open the command palette.
|
||||||
|
</CardDescription>
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
placeholder="Filter..."
|
||||||
|
onChange={(e) => setFilter(e.target.value)}
|
||||||
|
className="w-full md:w-56 lg:w-80 ml-auto px-4"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="max-sm:p-2">
|
||||||
|
<Suspense>
|
||||||
|
<SystemsTable filter={filter} />
|
||||||
|
</Suspense>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
{hubVersion && (
|
||||||
|
<div className="flex gap-1.5 justify-end items-center pr-3 sm:pr-6 mt-3.5 text-xs opacity-80">
|
||||||
|
<a
|
||||||
|
href="https://github.com/henrygd/beszel"
|
||||||
|
target="_blank"
|
||||||
|
className="flex items-center gap-0.5 text-muted-foreground hover:text-foreground duration-75"
|
||||||
|
>
|
||||||
|
<GithubIcon className="h-3 w-3" /> GitHub
|
||||||
|
</a>
|
||||||
|
<Separator orientation="vertical" className="h-2.5 bg-muted-foreground opacity-70" />
|
||||||
|
<a
|
||||||
|
href="https://github.com/henrygd/beszel/releases"
|
||||||
|
target="_blank"
|
||||||
|
className="text-muted-foreground hover:text-foreground duration-75"
|
||||||
|
>
|
||||||
|
Beszel {hubVersion}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
491
beszel/site/src/components/routes/system.tsx
Normal file
@@ -0,0 +1,491 @@
|
|||||||
|
import { $systems, pb, $chartTime, $containerFilter } from '@/lib/stores'
|
||||||
|
import { ContainerStatsRecord, SystemRecord, SystemStatsRecord } from '@/types'
|
||||||
|
import { Suspense, lazy, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
|
import { Card, CardHeader, CardTitle, CardDescription, CardContent } from '../ui/card'
|
||||||
|
import { useStore } from '@nanostores/react'
|
||||||
|
import Spinner from '../spinner'
|
||||||
|
import {
|
||||||
|
ClockArrowUp,
|
||||||
|
CpuIcon,
|
||||||
|
GlobeIcon,
|
||||||
|
LayoutGridIcon,
|
||||||
|
StretchHorizontalIcon,
|
||||||
|
XIcon,
|
||||||
|
} from 'lucide-react'
|
||||||
|
import ChartTimeSelect from '../charts/chart-time-select'
|
||||||
|
import {
|
||||||
|
chartTimeData,
|
||||||
|
cn,
|
||||||
|
getPbTimestamp,
|
||||||
|
useClampedIsInViewport,
|
||||||
|
useLocalStorage,
|
||||||
|
} from '@/lib/utils'
|
||||||
|
import { Separator } from '../ui/separator'
|
||||||
|
import { scaleTime } from 'd3-scale'
|
||||||
|
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 ContainerCpuChart = lazy(() => import('../charts/container-cpu-chart'))
|
||||||
|
const MemChart = lazy(() => import('../charts/mem-chart'))
|
||||||
|
const ContainerMemChart = lazy(() => import('../charts/container-mem-chart'))
|
||||||
|
const DiskChart = lazy(() => import('../charts/disk-chart'))
|
||||||
|
const DiskIoChart = lazy(() => import('../charts/disk-io-chart'))
|
||||||
|
const BandwidthChart = lazy(() => import('../charts/bandwidth-chart'))
|
||||||
|
const ContainerNetChart = lazy(() => import('../charts/container-net-chart'))
|
||||||
|
const SwapChart = lazy(() => import('../charts/swap-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 }) {
|
||||||
|
const systems = useStore($systems)
|
||||||
|
const chartTime = useStore($chartTime)
|
||||||
|
const [grid, setGrid] = useLocalStorage('grid', true)
|
||||||
|
const [ticks, setTicks] = useState([] as number[])
|
||||||
|
const [system, setSystem] = useState({} as SystemRecord)
|
||||||
|
const [systemStats, setSystemStats] = useState([] as SystemStatsRecord[])
|
||||||
|
const [hasDockerStats, setHasDocker] = useState(false)
|
||||||
|
const netCardRef = useRef<HTMLDivElement>(null)
|
||||||
|
const [dockerCpuChartData, setDockerCpuChartData] = useState<Record<string, number | string>[]>(
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
const [dockerMemChartData, setDockerMemChartData] = useState<Record<string, number | string>[]>(
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
const [dockerNetChartData, setDockerNetChartData] = useState<Record<string, number | number[]>[]>(
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
document.title = `${name} / Beszel`
|
||||||
|
return () => {
|
||||||
|
resetCharts()
|
||||||
|
$chartTime.set('1h')
|
||||||
|
$containerFilter.set('')
|
||||||
|
setHasDocker(false)
|
||||||
|
}
|
||||||
|
}, [name])
|
||||||
|
|
||||||
|
function resetCharts() {
|
||||||
|
setSystemStats([])
|
||||||
|
setDockerCpuChartData([])
|
||||||
|
setDockerMemChartData([])
|
||||||
|
setDockerNetChartData([])
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(resetCharts, [chartTime])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (system.id && system.name === name) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const matchingSystem = systems.find((s) => s.name === name) as SystemRecord
|
||||||
|
if (matchingSystem) {
|
||||||
|
setSystem(matchingSystem)
|
||||||
|
}
|
||||||
|
}, [name, system, systems])
|
||||||
|
|
||||||
|
// update system when new data is available
|
||||||
|
useEffect(() => {
|
||||||
|
if (!system.id) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
pb.collection<SystemRecord>('systems').subscribe(system.id, (e) => {
|
||||||
|
setSystem(e.record)
|
||||||
|
})
|
||||||
|
return () => {
|
||||||
|
pb.collection('systems').unsubscribe(system.id)
|
||||||
|
}
|
||||||
|
}, [system])
|
||||||
|
|
||||||
|
async function getStats<T>(collection: string): Promise<T[]> {
|
||||||
|
return await pb.collection<T>(collection).getFullList({
|
||||||
|
filter: pb.filter('system={:id} && created > {:created} && type={:type}', {
|
||||||
|
id: system.id,
|
||||||
|
created: getPbTimestamp(chartTime),
|
||||||
|
type: chartTimeData[chartTime].type,
|
||||||
|
}),
|
||||||
|
fields: 'created,stats',
|
||||||
|
sort: 'created',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// add empty values between records to make gaps if interval is too large
|
||||||
|
function addEmptyValues<T extends SystemStatsRecord | ContainerStatsRecord>(
|
||||||
|
records: T[],
|
||||||
|
expectedInterval: number
|
||||||
|
) {
|
||||||
|
const modifiedRecords: T[] = []
|
||||||
|
let prevTime = 0
|
||||||
|
for (let i = 0; i < records.length; i++) {
|
||||||
|
const record = records[i]
|
||||||
|
record.created = new Date(record.created).getTime()
|
||||||
|
if (prevTime) {
|
||||||
|
const interval = record.created - prevTime
|
||||||
|
// if interval is too large, add a null record
|
||||||
|
if (interval > expectedInterval / 2 + expectedInterval) {
|
||||||
|
// @ts-ignore
|
||||||
|
modifiedRecords.push({ created: null, stats: null })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
prevTime = record.created
|
||||||
|
modifiedRecords.push(record)
|
||||||
|
}
|
||||||
|
return modifiedRecords
|
||||||
|
}
|
||||||
|
|
||||||
|
// get stats
|
||||||
|
useEffect(() => {
|
||||||
|
if (!system.id || !chartTime) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
Promise.allSettled([
|
||||||
|
getStats<SystemStatsRecord>('system_stats'),
|
||||||
|
getStats<ContainerStatsRecord>('container_stats'),
|
||||||
|
]).then(([systemStats, containerStats]) => {
|
||||||
|
const expectedInterval = chartTimeData[chartTime].expectedInterval
|
||||||
|
if (containerStats.status === 'fulfilled' && containerStats.value.length) {
|
||||||
|
makeContainerData(addEmptyValues(containerStats.value, expectedInterval))
|
||||||
|
setHasDocker(true)
|
||||||
|
}
|
||||||
|
if (systemStats.status === 'fulfilled') {
|
||||||
|
setSystemStats(addEmptyValues(systemStats.value, expectedInterval))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}, [system, chartTime])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!systemStats.length) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const now = new Date()
|
||||||
|
const startTime = chartTimeData[chartTime].getOffset(now)
|
||||||
|
const scale = scaleTime([startTime.getTime(), now], [0, systemStats.length])
|
||||||
|
setTicks(scale.ticks(chartTimeData[chartTime].ticks).map((d) => d.getTime()))
|
||||||
|
}, [chartTime, systemStats])
|
||||||
|
|
||||||
|
// make container stats for charts
|
||||||
|
const makeContainerData = useCallback((containers: ContainerStatsRecord[]) => {
|
||||||
|
// console.log('containers', containers)
|
||||||
|
const dockerCpuData = []
|
||||||
|
const dockerMemData = []
|
||||||
|
const dockerNetData = []
|
||||||
|
for (let { created, stats } of containers) {
|
||||||
|
if (!created) {
|
||||||
|
let nullData = { time: null } as unknown
|
||||||
|
dockerCpuData.push(nullData as Record<string, number | string>)
|
||||||
|
dockerMemData.push(nullData as Record<string, number | string>)
|
||||||
|
dockerNetData.push(nullData as Record<string, number | number[]>)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const time = new Date(created).getTime()
|
||||||
|
let cpuData = { time } as Record<string, number | string>
|
||||||
|
let memData = { time } as Record<string, number | string>
|
||||||
|
let netData = { time } as Record<string, number | number[]>
|
||||||
|
for (let container of stats) {
|
||||||
|
cpuData[container.n] = container.c
|
||||||
|
memData[container.n] = container.m
|
||||||
|
netData[container.n] = [container.ns, container.nr, container.ns + container.nr] // sent, received, total
|
||||||
|
}
|
||||||
|
dockerCpuData.push(cpuData)
|
||||||
|
dockerMemData.push(memData)
|
||||||
|
dockerNetData.push(netData)
|
||||||
|
}
|
||||||
|
setDockerCpuChartData(dockerCpuData)
|
||||||
|
setDockerMemChartData(dockerMemData)
|
||||||
|
setDockerNetChartData(dockerNetData)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const uptime = useMemo(() => {
|
||||||
|
let uptime = system.info?.u || 0
|
||||||
|
if (uptime < 172800) {
|
||||||
|
return `${Math.trunc(uptime / 3600)} hours`
|
||||||
|
}
|
||||||
|
return `${Math.trunc(system.info?.u / 86400)} days`
|
||||||
|
}, [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) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<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>
|
||||||
|
<h1 className="text-[1.6rem] font-semibold mb-1.5">{system.name}</h1>
|
||||||
|
<div className="flex flex-wrap items-center gap-3 gap-y-2 text-sm opacity-90">
|
||||||
|
<div className="capitalize flex gap-2 items-center">
|
||||||
|
<span className={cn('relative flex h-3 w-3')}>
|
||||||
|
{system.status === 'up' && (
|
||||||
|
<span
|
||||||
|
className="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75"
|
||||||
|
style={{ animationDuration: '1.5s' }}
|
||||||
|
></span>
|
||||||
|
)}
|
||||||
|
<span
|
||||||
|
className={cn('relative inline-flex rounded-full h-3 w-3', {
|
||||||
|
'bg-green-500': system.status === 'up',
|
||||||
|
'bg-red-500': system.status === 'down',
|
||||||
|
'bg-primary/40': system.status === 'paused',
|
||||||
|
'bg-yellow-500': system.status === 'pending',
|
||||||
|
})}
|
||||||
|
></span>
|
||||||
|
</span>
|
||||||
|
{system.status}
|
||||||
|
</div>
|
||||||
|
<Separator orientation="vertical" className="h-4 bg-primary/30" />
|
||||||
|
<div className="flex gap-1.5 items-center">
|
||||||
|
<GlobeIcon className="h-4 w-4" /> {system.host}
|
||||||
|
</div>
|
||||||
|
{system.info?.u && (
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip>
|
||||||
|
<Separator orientation="vertical" className="h-4 bg-primary/30" />
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<div className="flex gap-1.5">
|
||||||
|
<ClockArrowUp className="h-4 w-4 mt-[1px]" /> {uptime}
|
||||||
|
</div>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>Uptime</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
)}
|
||||||
|
{system.info?.m && (
|
||||||
|
<>
|
||||||
|
<Separator orientation="vertical" className="h-4 bg-primary/30" />
|
||||||
|
<div className="flex gap-1.5">
|
||||||
|
<CpuIcon className="h-4 w-4 mt-[1px]" />
|
||||||
|
{system.info.m} ({system.info.c}c / {system.info.t}t)
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<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>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 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} />
|
||||||
|
</ChartCard>
|
||||||
|
|
||||||
|
{hasDockerStats && (
|
||||||
|
<ChartCard
|
||||||
|
grid={grid}
|
||||||
|
title="Docker CPU Usage"
|
||||||
|
description="CPU utilization of docker containers"
|
||||||
|
isContainerChart={true}
|
||||||
|
>
|
||||||
|
<ContainerCpuChart chartData={dockerCpuChartData} ticks={ticks} />
|
||||||
|
</ChartCard>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<ChartCard
|
||||||
|
grid={grid}
|
||||||
|
title="Total Memory Usage"
|
||||||
|
description="Precise utilization at the recorded time"
|
||||||
|
>
|
||||||
|
<MemChart ticks={ticks} systemData={systemStats} />
|
||||||
|
</ChartCard>
|
||||||
|
|
||||||
|
{hasDockerStats && (
|
||||||
|
<ChartCard
|
||||||
|
grid={grid}
|
||||||
|
title="Docker Memory Usage"
|
||||||
|
description="Memory usage of docker containers"
|
||||||
|
isContainerChart={true}
|
||||||
|
>
|
||||||
|
<ContainerMemChart chartData={dockerMemChartData} ticks={ticks} />
|
||||||
|
</ChartCard>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{(systemStats.at(-1)?.stats.s ?? 0) > 0 && (
|
||||||
|
<ChartCard grid={grid} title="Swap Usage" description="Swap space used by the system">
|
||||||
|
<SwapChart ticks={ticks} systemData={systemStats} />
|
||||||
|
</ChartCard>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<ChartCard grid={grid} title="Disk Space" description="Usage of root partition">
|
||||||
|
<DiskChart ticks={ticks} systemData={systemStats} />
|
||||||
|
</ChartCard>
|
||||||
|
|
||||||
|
<ChartCard grid={grid} title="Disk I/O" description="Throughput of root filesystem">
|
||||||
|
<DiskIoChart ticks={ticks} systemData={systemStats} />
|
||||||
|
</ChartCard>
|
||||||
|
|
||||||
|
<ChartCard
|
||||||
|
grid={grid}
|
||||||
|
title="Bandwidth"
|
||||||
|
description="Network traffic of public interfaces"
|
||||||
|
>
|
||||||
|
<BandwidthChart ticks={ticks} systemData={systemStats} />
|
||||||
|
</ChartCard>
|
||||||
|
|
||||||
|
{hasDockerStats && dockerNetChartData.length > 0 && (
|
||||||
|
<div
|
||||||
|
ref={netCardRef}
|
||||||
|
className={cn({
|
||||||
|
'col-span-full': !grid,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<ChartCard
|
||||||
|
title="Docker Network I/O"
|
||||||
|
description="Includes traffic between internal services"
|
||||||
|
isContainerChart={true}
|
||||||
|
>
|
||||||
|
<ContainerNetChart chartData={dockerNetChartData} ticks={ticks} />
|
||||||
|
</ChartCard>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ChartCard({
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
children,
|
||||||
|
grid,
|
||||||
|
isContainerChart,
|
||||||
|
}: {
|
||||||
|
title: string
|
||||||
|
description: string
|
||||||
|
children: React.ReactNode
|
||||||
|
grid?: boolean
|
||||||
|
isContainerChart?: boolean
|
||||||
|
}) {
|
||||||
|
const target = useRef<HTMLDivElement>(null)
|
||||||
|
const [isInViewport, wrappedTargetRef] = useClampedIsInViewport({ target: target })
|
||||||
|
|
||||||
|
return (
|
||||||
|
<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">
|
||||||
|
<CardTitle className="text-xl sm:text-2xl">{title}</CardTitle>
|
||||||
|
<CardDescription>{description}</CardDescription>
|
||||||
|
{isContainerChart && <ContainerFilterBar />}
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="pl-0 w-[calc(100%-1.6em)] h-52 relative">
|
||||||
|
{<Spinner />}
|
||||||
|
{isInViewport && <Suspense>{children}</Suspense>}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -21,7 +21,6 @@ import {
|
|||||||
} from '@/components/ui/table'
|
} from '@/components/ui/table'
|
||||||
|
|
||||||
import { Button, buttonVariants } from '@/components/ui/button'
|
import { Button, buttonVariants } from '@/components/ui/button'
|
||||||
import { Input } from '@/components/ui/input'
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
@@ -55,11 +54,11 @@ import {
|
|||||||
PauseCircleIcon,
|
PauseCircleIcon,
|
||||||
PlayCircleIcon,
|
PlayCircleIcon,
|
||||||
Trash2Icon,
|
Trash2Icon,
|
||||||
|
WifiIcon,
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import { useMemo, useState } from 'react'
|
import { useEffect, useMemo, useState } from 'react'
|
||||||
import { $systems, pb } from '@/lib/stores'
|
import { $hubVersion, $systems, pb } from '@/lib/stores'
|
||||||
import { useStore } from '@nanostores/react'
|
import { useStore } from '@nanostores/react'
|
||||||
import { AddSystemButton } from '../add-system'
|
|
||||||
import { cn, copyToClipboard, isReadOnlyUser } from '@/lib/utils'
|
import { cn, copyToClipboard, isReadOnlyUser } from '@/lib/utils'
|
||||||
import AlertsButton from '../table-alerts'
|
import AlertsButton from '../table-alerts'
|
||||||
import { navigate } from '../router'
|
import { navigate } from '../router'
|
||||||
@@ -73,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>
|
||||||
@@ -82,7 +81,12 @@ function CellFormatter(info: CellContext<SystemRecord, unknown>) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function sortableHeader(column: Column<SystemRecord, unknown>, name: string, Icon: any) {
|
function sortableHeader(
|
||||||
|
column: Column<SystemRecord, unknown>,
|
||||||
|
name: string,
|
||||||
|
Icon: any,
|
||||||
|
hideSortIcon = false
|
||||||
|
) {
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
@@ -91,16 +95,23 @@ function sortableHeader(column: Column<SystemRecord, unknown>, name: string, Ico
|
|||||||
>
|
>
|
||||||
<Icon className="mr-2 h-4 w-4" />
|
<Icon className="mr-2 h-4 w-4" />
|
||||||
{name}
|
{name}
|
||||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
{!hideSortIcon && <ArrowUpDown className="ml-2 h-4 w-4" />}
|
||||||
</Button>
|
</Button>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function SystemsTable() {
|
export default function SystemsTable({ filter }: { filter?: string }) {
|
||||||
const data = useStore($systems)
|
const data = useStore($systems)
|
||||||
|
const hubVersion = useStore($hubVersion)
|
||||||
const [sorting, setSorting] = useState<SortingState>([])
|
const [sorting, setSorting] = useState<SortingState>([])
|
||||||
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([])
|
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (filter !== undefined) {
|
||||||
|
table.getColumn('name')?.setFilterValue(filter)
|
||||||
|
}
|
||||||
|
}, [filter])
|
||||||
|
|
||||||
const columns: ColumnDef<SystemRecord>[] = useMemo(() => {
|
const columns: ColumnDef<SystemRecord>[] = useMemo(() => {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
@@ -124,7 +135,7 @@ export default function SystemsTable() {
|
|||||||
<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}
|
||||||
@@ -150,6 +161,29 @@ export default function SystemsTable() {
|
|||||||
cell: CellFormatter,
|
cell: CellFormatter,
|
||||||
header: ({ column }) => sortableHeader(column, 'Disk', HardDrive),
|
header: ({ column }) => sortableHeader(column, 'Disk', HardDrive),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'info.v',
|
||||||
|
size: 50,
|
||||||
|
cell: (info) => {
|
||||||
|
const version = info.getValue() as string
|
||||||
|
if (!version || !hubVersion) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<span className="flex gap-2 items-center md:pr-5 tabular-nums pl-1">
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
'w-2 h-2 left-0 rounded-full',
|
||||||
|
version === hubVersion ? 'bg-green-500' : 'bg-yellow-500'
|
||||||
|
)}
|
||||||
|
style={{ marginBottom: '-1px' }}
|
||||||
|
></span>
|
||||||
|
<span>{info.getValue() as string}</span>
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
header: ({ column }) => sortableHeader(column, 'Agent', WifiIcon, true),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'actions',
|
id: 'actions',
|
||||||
size: 120,
|
size: 120,
|
||||||
@@ -226,7 +260,7 @@ export default function SystemsTable() {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
}, [])
|
}, [hubVersion])
|
||||||
|
|
||||||
const table = useReactTable({
|
const table = useReactTable({
|
||||||
data,
|
data,
|
||||||
@@ -248,80 +282,64 @@ export default function SystemsTable() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="rounded-md border overflow-hidden">
|
||||||
<div className="w-full">
|
<Table>
|
||||||
<div className="flex items-center mb-4 gap-2">
|
<TableHeader className="bg-muted/40">
|
||||||
<Input
|
{table.getHeaderGroups().map((headerGroup) => (
|
||||||
// @ts-ignore
|
<TableRow key={headerGroup.id}>
|
||||||
placeholder="Filter..."
|
{headerGroup.headers.map((header) => {
|
||||||
value={(table.getColumn('name')?.getFilterValue() as string) ?? ''}
|
return (
|
||||||
onChange={(event) => table.getColumn('name')?.setFilterValue(event.target.value)}
|
<TableHead className="px-2" key={header.id}>
|
||||||
className="max-w-sm"
|
{header.isPlaceholder
|
||||||
/>
|
? null
|
||||||
<div className={cn('ml-auto flex gap-2', isReadOnlyUser() && 'hidden')}>
|
: flexRender(header.column.columnDef.header, header.getContext())}
|
||||||
<AddSystemButton />
|
</TableHead>
|
||||||
</div>
|
)
|
||||||
</div>
|
})}
|
||||||
<div className="rounded-md border overflow-hidden">
|
</TableRow>
|
||||||
<Table>
|
))}
|
||||||
<TableHeader className="bg-muted/40">
|
</TableHeader>
|
||||||
{table.getHeaderGroups().map((headerGroup) => (
|
<TableBody>
|
||||||
<TableRow key={headerGroup.id}>
|
{table.getRowModel().rows?.length ? (
|
||||||
{headerGroup.headers.map((header) => {
|
table.getRowModel().rows.map((row) => (
|
||||||
return (
|
<TableRow
|
||||||
<TableHead className="px-2" key={header.id}>
|
key={row.original.id}
|
||||||
{header.isPlaceholder
|
data-state={row.getIsSelected() && 'selected'}
|
||||||
? null
|
className={cn('cursor-pointer transition-opacity', {
|
||||||
: flexRender(header.column.columnDef.header, header.getContext())}
|
'opacity-50': row.original.status === 'paused',
|
||||||
</TableHead>
|
})}
|
||||||
)
|
onClick={(e) => {
|
||||||
})}
|
const target = e.target as HTMLElement
|
||||||
</TableRow>
|
if (!target.closest('[data-nolink]') && e.currentTarget.contains(target)) {
|
||||||
))}
|
navigate(`/system/${encodeURIComponent(row.original.name)}`)
|
||||||
</TableHeader>
|
}
|
||||||
<TableBody>
|
}}
|
||||||
{table.getRowModel().rows?.length ? (
|
>
|
||||||
table.getRowModel().rows.map((row) => (
|
{row.getVisibleCells().map((cell) => (
|
||||||
<TableRow
|
<TableCell
|
||||||
key={row.original.id}
|
key={cell.id}
|
||||||
data-state={row.getIsSelected() && 'selected'}
|
style={{
|
||||||
className={cn('cursor-pointer transition-opacity', {
|
width:
|
||||||
'opacity-50': row.original.status === 'paused',
|
cell.column.getSize() === Number.MAX_SAFE_INTEGER
|
||||||
})}
|
? 'auto'
|
||||||
onClick={(e) => {
|
: cell.column.getSize(),
|
||||||
const target = e.target as HTMLElement
|
|
||||||
if (!target.closest('[data-nolink]') && e.currentTarget.contains(target)) {
|
|
||||||
navigate(`/system/${row.original.name}`)
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
|
className={'overflow-hidden relative py-2.5'}
|
||||||
>
|
>
|
||||||
{row.getVisibleCells().map((cell) => (
|
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||||
<TableCell
|
|
||||||
key={cell.id}
|
|
||||||
style={{
|
|
||||||
width:
|
|
||||||
cell.column.getSize() === Number.MAX_SAFE_INTEGER
|
|
||||||
? 'auto'
|
|
||||||
: cell.column.getSize(),
|
|
||||||
}}
|
|
||||||
className={'overflow-hidden relative py-2.5'}
|
|
||||||
>
|
|
||||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
|
||||||
</TableCell>
|
|
||||||
))}
|
|
||||||
</TableRow>
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<TableRow>
|
|
||||||
<TableCell colSpan={columns.length} className="h-24 text-center">
|
|
||||||
No systems found
|
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
))}
|
||||||
)}
|
</TableRow>
|
||||||
</TableBody>
|
))
|
||||||
</Table>
|
) : (
|
||||||
</div>
|
<TableRow>
|
||||||
</div>
|
<TableCell colSpan={columns.length} className="h-24 text-center">
|
||||||
</>
|
No systems found
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -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 : '')}
|
||||||
@@ -258,7 +263,7 @@ const ChartLegendContent = React.forwardRef<
|
|||||||
nameKey?: string
|
nameKey?: string
|
||||||
}
|
}
|
||||||
>(({ className, hideIcon = false, payload, verticalAlign = 'bottom', nameKey }, ref) => {
|
>(({ className, hideIcon = false, payload, verticalAlign = 'bottom', nameKey }, ref) => {
|
||||||
const { config } = useChart()
|
// const { config } = useChart()
|
||||||
|
|
||||||
if (!payload?.length) {
|
if (!payload?.length) {
|
||||||
return null
|
return null
|
||||||
@@ -268,33 +273,35 @@ const ChartLegendContent = React.forwardRef<
|
|||||||
<div
|
<div
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex items-center justify-center gap-4',
|
'flex items-center justify-center gap-4 gap-y-1 flex-wrap',
|
||||||
verticalAlign === 'top' ? 'pb-3' : 'pt-3',
|
verticalAlign === 'top' ? 'pb-3' : 'pt-3',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{payload.map((item) => {
|
{payload.map((item) => {
|
||||||
const key = `${nameKey || item.dataKey || 'value'}`
|
// const key = `${nameKey || item.dataKey || 'value'}`
|
||||||
const itemConfig = getPayloadConfigFromPayload(config, item, key)
|
// const itemConfig = getPayloadConfigFromPayload(config, item, key)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={item.value}
|
key={item.value}
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground'
|
// 'flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground text-muted-foreground'
|
||||||
|
'flex items-center gap-1.5 text-muted-foreground'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{itemConfig?.icon && !hideIcon ? (
|
{/* {itemConfig?.icon && !hideIcon ? (
|
||||||
<itemConfig.icon />
|
<itemConfig.icon />
|
||||||
) : (
|
) : ( */}
|
||||||
<div
|
<div
|
||||||
className="h-2 w-2 shrink-0 rounded-[2px]"
|
className="h-2 w-2 shrink-0 rounded-[2px]"
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: item.color,
|
backgroundColor: item.color,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
{item.value}
|
||||||
{itemConfig?.label}
|
{/* )} */}
|
||||||
|
{/* {itemConfig?.label} */}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
@@ -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}
|
||||||
91
beszel/site/src/components/ui/table.tsx
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
import * as React from 'react'
|
||||||
|
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
const Table = React.forwardRef<HTMLTableElement, React.HTMLAttributes<HTMLTableElement>>(
|
||||||
|
({ className, ...props }, ref) => (
|
||||||
|
<div className="relative w-full overflow-auto">
|
||||||
|
<table ref={ref} className={cn('w-full caption-bottom text-sm', className)} {...props} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
)
|
||||||
|
Table.displayName = 'Table'
|
||||||
|
|
||||||
|
const TableHeader = React.forwardRef<
|
||||||
|
HTMLTableSectionElement,
|
||||||
|
React.HTMLAttributes<HTMLTableSectionElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<thead ref={ref} className={cn('[&_tr]:border-b', className)} {...props} />
|
||||||
|
))
|
||||||
|
TableHeader.displayName = 'TableHeader'
|
||||||
|
|
||||||
|
const TableBody = React.forwardRef<
|
||||||
|
HTMLTableSectionElement,
|
||||||
|
React.HTMLAttributes<HTMLTableSectionElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<tbody ref={ref} className={cn('[&_tr:last-child]:border-0', className)} {...props} />
|
||||||
|
))
|
||||||
|
TableBody.displayName = 'TableBody'
|
||||||
|
|
||||||
|
const TableFooter = React.forwardRef<
|
||||||
|
HTMLTableSectionElement,
|
||||||
|
React.HTMLAttributes<HTMLTableSectionElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<tfoot
|
||||||
|
ref={ref}
|
||||||
|
className={cn('border-t bg-muted/50 font-medium [&>tr]:last:border-b-0', className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
TableFooter.displayName = 'TableFooter'
|
||||||
|
|
||||||
|
const TableRow = React.forwardRef<HTMLTableRowElement, React.HTMLAttributes<HTMLTableRowElement>>(
|
||||||
|
({ className, ...props }, ref) => (
|
||||||
|
<tr
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
'border-b transition-colors hover:bg-muted/40 dark:hover:bg-muted/30 data-[state=selected]:bg-muted',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
)
|
||||||
|
TableRow.displayName = 'TableRow'
|
||||||
|
|
||||||
|
const TableHead = React.forwardRef<
|
||||||
|
HTMLTableCellElement,
|
||||||
|
React.ThHTMLAttributes<HTMLTableCellElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<th
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
'h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
TableHead.displayName = 'TableHead'
|
||||||
|
|
||||||
|
const TableCell = React.forwardRef<
|
||||||
|
HTMLTableCellElement,
|
||||||
|
React.TdHTMLAttributes<HTMLTableCellElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<td
|
||||||
|
ref={ref}
|
||||||
|
className={cn('p-4 align-middle [&:has([role=checkbox])]:pr-0', className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
TableCell.displayName = 'TableCell'
|
||||||
|
|
||||||
|
const TableCaption = React.forwardRef<
|
||||||
|
HTMLTableCaptionElement,
|
||||||
|
React.HTMLAttributes<HTMLTableCaptionElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<caption ref={ref} className={cn('mt-4 text-sm text-muted-foreground', className)} {...props} />
|
||||||
|
))
|
||||||
|
TableCaption.displayName = 'TableCaption'
|
||||||
|
|
||||||
|
export { Table, TableHeader, TableBody, TableFooter, TableHead, TableRow, TableCell, TableCaption }
|
||||||
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 }
|
||||||