combine hub and agent repos
2
.gitattributes
vendored
@@ -1 +1 @@
|
|||||||
*.tsx linguist-language=Go
|
*.ts linguist-language=Go
|
3
.gitignore
vendored
@@ -4,4 +4,5 @@ data
|
|||||||
temp
|
temp
|
||||||
.vscode
|
.vscode
|
||||||
beszel
|
beszel
|
||||||
beszel_data
|
beszel_data
|
||||||
|
dist
|
41
agent/.goreleaser.yml
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
# version: 1
|
||||||
|
|
||||||
|
project_name: beszel-agent
|
||||||
|
|
||||||
|
before:
|
||||||
|
hooks:
|
||||||
|
- go mod tidy
|
||||||
|
|
||||||
|
builds:
|
||||||
|
- env:
|
||||||
|
- CGO_ENABLED=0
|
||||||
|
ignore:
|
||||||
|
- goos: darwin
|
||||||
|
goarch: 386
|
||||||
|
- goos: linux
|
||||||
|
goarch: arm
|
||||||
|
- goos: linux
|
||||||
|
goarch: 386
|
||||||
|
- goos: windows
|
||||||
|
goarch: arm64
|
||||||
|
- goos: windows
|
||||||
|
goarch: 386
|
||||||
|
|
||||||
|
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:'
|
11
agent/docker-compose.yml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
services:
|
||||||
|
agent:
|
||||||
|
image: 'henrygd/qoma-agent'
|
||||||
|
container_name: 'qoma-agent'
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- '45876:45876'
|
||||||
|
volumes:
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
environment:
|
||||||
|
- KEY="ssh-ed25519 YOUR_PUBLIC_KEY"
|
20
agent/dockerfile
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
FROM --platform=$BUILDPLATFORM golang:alpine as builder
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Download Go modules
|
||||||
|
COPY go.mod go.sum ./
|
||||||
|
RUN go mod download
|
||||||
|
|
||||||
|
COPY *.go ./
|
||||||
|
|
||||||
|
# Build
|
||||||
|
ARG TARGETOS TARGETARCH
|
||||||
|
RUN CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -ldflags "-w -s" -o /agent .
|
||||||
|
|
||||||
|
# ? -------------------------
|
||||||
|
FROM scratch
|
||||||
|
|
||||||
|
COPY --from=builder /agent /agent
|
||||||
|
|
||||||
|
ENTRYPOINT ["/agent"]
|
21
agent/go.mod
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
module beszel-agent
|
||||||
|
|
||||||
|
go 1.22.4
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/gliderlabs/ssh v0.3.7
|
||||||
|
github.com/shirou/gopsutil/v4 v4.24.6
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect
|
||||||
|
github.com/go-ole/go-ole v1.3.0 // 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/tklauser/go-sysconf v0.3.14 // indirect
|
||||||
|
github.com/tklauser/numcpus v0.8.0 // indirect
|
||||||
|
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||||
|
golang.org/x/crypto v0.25.0 // indirect
|
||||||
|
golang.org/x/sys v0.22.0 // indirect
|
||||||
|
)
|
42
agent/go.sum
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
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/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/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/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||||
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
|
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/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/shirou/gopsutil/v4 v4.24.6 h1:9qqCSYF2pgOU+t+NgJtp7Co5+5mHF/HyKBUckySQL64=
|
||||||
|
github.com/shirou/gopsutil/v4 v4.24.6/go.mod h1:aoebb2vxetJ/yIDZISmduFvVNPHqXQ9SEJwRXxkf0RA=
|
||||||
|
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/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/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.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
|
||||||
|
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
|
||||||
|
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/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.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
|
||||||
|
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk=
|
||||||
|
golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
399
agent/main.go
Normal file
@@ -0,0 +1,399 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"math"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"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 containerCpuMap = make(map[string][2]uint64)
|
||||||
|
var containerCpuMutex = &sync.Mutex{}
|
||||||
|
|
||||||
|
var diskIoStats = DiskIoStats{
|
||||||
|
Read: 0,
|
||||||
|
Write: 0,
|
||||||
|
Time: time.Now(),
|
||||||
|
Filesystem: "",
|
||||||
|
}
|
||||||
|
|
||||||
|
var netIoStats = NetIoStats{
|
||||||
|
BytesRecv: 0,
|
||||||
|
BytesSent: 0,
|
||||||
|
Time: time.Now(),
|
||||||
|
Name: "",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a custom HTTP transport
|
||||||
|
var transport = &http.Transport{
|
||||||
|
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||||
|
d := net.Dialer{
|
||||||
|
Timeout: 5 * time.Second,
|
||||||
|
}
|
||||||
|
// Connect to the Unix socket
|
||||||
|
return d.DialContext(ctx, "unix", "/var/run/docker.sock")
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
type SystemData struct {
|
||||||
|
Stats SystemStats `json:"stats"`
|
||||||
|
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"`
|
||||||
|
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"`
|
||||||
|
// MemPct float64 `json:"mp"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func getSystemStats() (SystemInfo, SystemStats) {
|
||||||
|
c, _ := cpu.Percent(0, false)
|
||||||
|
v, _ := mem.VirtualMemory()
|
||||||
|
d, _ := disk.Usage("/")
|
||||||
|
|
||||||
|
cpuPct := twoDecimals(c[0])
|
||||||
|
memPct := twoDecimals(v.UsedPercent)
|
||||||
|
diskPct := twoDecimals(d.UsedPercent)
|
||||||
|
|
||||||
|
systemStats := SystemStats{
|
||||||
|
Cpu: cpuPct,
|
||||||
|
Mem: bytesToGigabytes(v.Total),
|
||||||
|
MemUsed: bytesToGigabytes(v.Used),
|
||||||
|
MemBuffCache: bytesToGigabytes(v.Total - v.Free - v.Used),
|
||||||
|
MemPct: memPct,
|
||||||
|
Disk: bytesToGigabytes(d.Total),
|
||||||
|
DiskUsed: bytesToGigabytes(d.Used),
|
||||||
|
DiskPct: diskPct,
|
||||||
|
}
|
||||||
|
|
||||||
|
systemInfo := SystemInfo{
|
||||||
|
Cpu: cpuPct,
|
||||||
|
MemPct: memPct,
|
||||||
|
DiskPct: diskPct,
|
||||||
|
}
|
||||||
|
|
||||||
|
// add disk stats
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// add network stats
|
||||||
|
if netIO, err := psutilNet.IOCounters(true); err == nil {
|
||||||
|
bytesSent := uint64(0)
|
||||||
|
bytesRecv := uint64(0)
|
||||||
|
for _, v := range netIO {
|
||||||
|
if skipNetworkInterface(v.Name) {
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
|
||||||
|
// add host stats
|
||||||
|
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 {
|
||||||
|
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) {
|
||||||
|
client := &http.Client{
|
||||||
|
Transport: transport,
|
||||||
|
}
|
||||||
|
// Create a new HTTP request
|
||||||
|
req, err := http.NewRequest("GET", "http://localhost/containers/json", nil)
|
||||||
|
if err != nil {
|
||||||
|
return []ContainerStats{}, err
|
||||||
|
}
|
||||||
|
// Perform the request
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return []ContainerStats{}, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
var containers []Container
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&containers); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
var containerStats []ContainerStats
|
||||||
|
|
||||||
|
for _, ctr := range containers {
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
cstats, err := getContainerStats(ctr)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error getting container stats: %+v\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
containerStats = append(containerStats, cstats)
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
// clean up old containers from map
|
||||||
|
validNames := make(map[string]struct{}, len(containers))
|
||||||
|
for _, ctr := range containers {
|
||||||
|
validNames[ctr.Names[0][1:]] = struct{}{}
|
||||||
|
}
|
||||||
|
for name := range containerCpuMap {
|
||||||
|
if _, exists := validNames[name]; !exists {
|
||||||
|
delete(containerCpuMap, name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
|
return containerStats, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getContainerStats(ctr Container) (ContainerStats, error) {
|
||||||
|
// stats, _ := apiClient.ContainerStats(context.Background(), ctr.ID, false)
|
||||||
|
client := &http.Client{
|
||||||
|
Transport: transport,
|
||||||
|
}
|
||||||
|
// Create a new HTTP request
|
||||||
|
req, err := http.NewRequest("GET", "http://localhost/containers/"+ctr.ID+"/stats?stream=0&one-shot=1", nil)
|
||||||
|
if err != nil {
|
||||||
|
return ContainerStats{}, err
|
||||||
|
}
|
||||||
|
// Perform the request
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
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
|
||||||
|
usedMemory := statsJson.MemoryStats.Usage - statsJson.MemoryStats.Cache
|
||||||
|
// pctMemory := float64(usedMemory) / float64(statsJson.MemoryStats.Limit) * 100
|
||||||
|
|
||||||
|
// cpu
|
||||||
|
// add default values to containerCpu if it doesn't exist
|
||||||
|
containerCpuMutex.Lock()
|
||||||
|
defer containerCpuMutex.Unlock()
|
||||||
|
if _, ok := containerCpuMap[name]; !ok {
|
||||||
|
containerCpuMap[name] = [2]uint64{0, 0}
|
||||||
|
}
|
||||||
|
cpuDelta := statsJson.CPUStats.CPUUsage.TotalUsage - containerCpuMap[name][0]
|
||||||
|
systemDelta := statsJson.CPUStats.SystemUsage - containerCpuMap[name][1]
|
||||||
|
cpuPct := float64(cpuDelta) / float64(systemDelta) * 100
|
||||||
|
if cpuPct > 100 {
|
||||||
|
return ContainerStats{}, errors.New("cpu pct is greater than 100")
|
||||||
|
}
|
||||||
|
containerCpuMap[name] = [2]uint64{statsJson.CPUStats.CPUUsage.TotalUsage, statsJson.CPUStats.SystemUsage}
|
||||||
|
|
||||||
|
cStats := ContainerStats{
|
||||||
|
Name: name,
|
||||||
|
Cpu: twoDecimals(cpuPct),
|
||||||
|
Mem: bytesToMegabytes(float64(usedMemory)),
|
||||||
|
// MemPct: maxDecimals(pctMemory, 2),
|
||||||
|
}
|
||||||
|
return cStats, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
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(port 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 port %s", port)
|
||||||
|
if err := sshServer.ListenAndServe(":"+port, 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() {
|
||||||
|
var pubKey []byte
|
||||||
|
if pubKeyEnv, exists := os.LookupEnv("KEY"); exists {
|
||||||
|
pubKey = []byte(pubKeyEnv)
|
||||||
|
} else {
|
||||||
|
pubKey = []byte("ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJgPK8kpPOwPFIq6BIa7Bu/xwrjt5VRQCz3az3Glt4jp")
|
||||||
|
// 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 {
|
||||||
|
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(name string) bool {
|
||||||
|
return strings.HasPrefix(name, "lo") || strings.HasPrefix(name, "docker") || strings.HasPrefix(name, "br-") || strings.HasPrefix(name, "veth")
|
||||||
|
}
|
||||||
|
|
||||||
|
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.Name) {
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
116
agent/types.go
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
type Container struct {
|
||||||
|
ID string `json:"Id"`
|
||||||
|
Names []string
|
||||||
|
Image string
|
||||||
|
ImageID string
|
||||||
|
Command string
|
||||||
|
Created int64
|
||||||
|
// Ports []Port
|
||||||
|
SizeRw int64 `json:",omitempty"`
|
||||||
|
SizeRootFs int64 `json:",omitempty"`
|
||||||
|
Labels map[string]string
|
||||||
|
State string
|
||||||
|
Status string
|
||||||
|
HostConfig struct {
|
||||||
|
NetworkMode string `json:",omitempty"`
|
||||||
|
Annotations map[string]string `json:",omitempty"`
|
||||||
|
}
|
||||||
|
// NetworkSettings *SummaryNetworkSettings
|
||||||
|
// Mounts []MountPoint
|
||||||
|
}
|
||||||
|
|
||||||
|
type CStats struct {
|
||||||
|
// Common stats
|
||||||
|
Read time.Time `json:"read"`
|
||||||
|
PreRead time.Time `json:"preread"`
|
||||||
|
|
||||||
|
// Linux specific stats, not populated on Windows.
|
||||||
|
// PidsStats PidsStats `json:"pids_stats,omitempty"`
|
||||||
|
// BlkioStats BlkioStats `json:"blkio_stats,omitempty"`
|
||||||
|
|
||||||
|
// Windows specific stats, not populated on Linux.
|
||||||
|
NumProcs uint32 `json:"num_procs"`
|
||||||
|
// StorageStats StorageStats `json:"storage_stats,omitempty"`
|
||||||
|
|
||||||
|
// Shared stats
|
||||||
|
CPUStats CPUStats `json:"cpu_stats,omitempty"`
|
||||||
|
PreCPUStats CPUStats `json:"precpu_stats,omitempty"` // "Pre"="Previous"
|
||||||
|
MemoryStats MemoryStats `json:"memory_stats,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CPUStats struct {
|
||||||
|
// CPU Usage. Linux and Windows.
|
||||||
|
CPUUsage CPUUsage `json:"cpu_usage"`
|
||||||
|
|
||||||
|
// System Usage. Linux only.
|
||||||
|
SystemUsage uint64 `json:"system_cpu_usage,omitempty"`
|
||||||
|
|
||||||
|
// Online CPUs. Linux only.
|
||||||
|
OnlineCPUs uint32 `json:"online_cpus,omitempty"`
|
||||||
|
|
||||||
|
// Throttling Data. Linux only.
|
||||||
|
// ThrottlingData ThrottlingData `json:"throttling_data,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CPUUsage struct {
|
||||||
|
// Total CPU time consumed.
|
||||||
|
// Units: nanoseconds (Linux)
|
||||||
|
// Units: 100's of nanoseconds (Windows)
|
||||||
|
TotalUsage uint64 `json:"total_usage"`
|
||||||
|
|
||||||
|
// Total CPU time consumed per core (Linux). Not used on Windows.
|
||||||
|
// Units: nanoseconds.
|
||||||
|
PercpuUsage []uint64 `json:"percpu_usage,omitempty"`
|
||||||
|
|
||||||
|
// Time spent by tasks of the cgroup in kernel mode (Linux).
|
||||||
|
// Time spent by all container processes in kernel mode (Windows).
|
||||||
|
// Units: nanoseconds (Linux).
|
||||||
|
// Units: 100's of nanoseconds (Windows). Not populated for Hyper-V Containers.
|
||||||
|
UsageInKernelmode uint64 `json:"usage_in_kernelmode"`
|
||||||
|
|
||||||
|
// Time spent by tasks of the cgroup in user mode (Linux).
|
||||||
|
// Time spent by all container processes in user mode (Windows).
|
||||||
|
// Units: nanoseconds (Linux).
|
||||||
|
// Units: 100's of nanoseconds (Windows). Not populated for Hyper-V Containers
|
||||||
|
UsageInUsermode uint64 `json:"usage_in_usermode"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type MemoryStats struct {
|
||||||
|
|
||||||
|
// current res_counter usage for memory
|
||||||
|
Usage uint64 `json:"usage,omitempty"`
|
||||||
|
Cache uint64 `json:"cache,omitempty"`
|
||||||
|
// maximum usage ever recorded.
|
||||||
|
MaxUsage uint64 `json:"max_usage,omitempty"`
|
||||||
|
// TODO(vishh): Export these as stronger types.
|
||||||
|
// all the stats exported via memory.stat.
|
||||||
|
Stats map[string]uint64 `json:"stats,omitempty"`
|
||||||
|
// number of times memory usage hits limits.
|
||||||
|
Failcnt uint64 `json:"failcnt,omitempty"`
|
||||||
|
Limit uint64 `json:"limit,omitempty"`
|
||||||
|
|
||||||
|
// committed bytes
|
||||||
|
Commit uint64 `json:"commitbytes,omitempty"`
|
||||||
|
// peak committed bytes
|
||||||
|
CommitPeak uint64 `json:"commitpeakbytes,omitempty"`
|
||||||
|
// private working set
|
||||||
|
PrivateWorkingSet uint64 `json:"privateworkingset,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DiskIoStats struct {
|
||||||
|
Read uint64
|
||||||
|
Write uint64
|
||||||
|
Time time.Time
|
||||||
|
Filesystem string
|
||||||
|
}
|
||||||
|
|
||||||
|
type NetIoStats struct {
|
||||||
|
BytesRecv uint64
|
||||||
|
BytesSent uint64
|
||||||
|
Time time.Time
|
||||||
|
Name string
|
||||||
|
}
|
41
hub/.goreleaser.yml
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
# version: 1
|
||||||
|
|
||||||
|
project_name: beszel
|
||||||
|
|
||||||
|
before:
|
||||||
|
hooks:
|
||||||
|
- go mod tidy
|
||||||
|
|
||||||
|
builds:
|
||||||
|
- env:
|
||||||
|
- CGO_ENABLED=0
|
||||||
|
ignore:
|
||||||
|
- goos: darwin
|
||||||
|
goarch: 386
|
||||||
|
- goos: linux
|
||||||
|
goarch: arm
|
||||||
|
- goos: linux
|
||||||
|
goarch: 386
|
||||||
|
- goos: windows
|
||||||
|
goarch: arm64
|
||||||
|
- goos: windows
|
||||||
|
goarch: 386
|
||||||
|
|
||||||
|
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:'
|
@@ -11,7 +11,7 @@ COPY migrations ./migrations
|
|||||||
|
|
||||||
# Build
|
# Build
|
||||||
ARG TARGETOS TARGETARCH
|
ARG TARGETOS TARGETARCH
|
||||||
RUN CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -ldflags "-w -s" -o /server .
|
RUN CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -ldflags "-w -s" -o /beszel .
|
||||||
|
|
||||||
# ? -------------------------
|
# ? -------------------------
|
||||||
FROM alpine:latest
|
FROM alpine:latest
|
0
site/.gitignore → hub/site/.gitignore
vendored
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 |