mirror of
https://github.com/henrygd/beszel.git
synced 2026-03-21 21:26:16 +01:00
Compare commits
20 Commits
v0.0.1-alp
...
v0.1.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
26dbb1968a | ||
|
|
ee57e84cb8 | ||
|
|
345dbeb757 | ||
|
|
29f5d3ae62 | ||
|
|
d4b0887153 | ||
|
|
06e4dd10e0 | ||
|
|
af4d5137d6 | ||
|
|
5e255f8f69 | ||
|
|
76cfaaa179 | ||
|
|
b89bec31b5 | ||
|
|
0355d9c654 | ||
|
|
41df7b7392 | ||
|
|
52c77dd361 | ||
|
|
ae0f5c938f | ||
|
|
78dc269538 | ||
|
|
f6967eab35 | ||
|
|
e787b6ea1b | ||
|
|
844b95dfd0 | ||
|
|
c5776541a0 | ||
|
|
5ba7568acf |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -6,4 +6,5 @@ temp
|
||||
beszel
|
||||
beszel-agent
|
||||
beszel_data
|
||||
beszel_data*
|
||||
dist
|
||||
@@ -5,7 +5,7 @@ services:
|
||||
restart: unless-stopped
|
||||
network_mode: host
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
environment:
|
||||
PORT: 45876
|
||||
KEY: 'ssh-ed25519 YOUR_PUBLIC_KEY'
|
||||
|
||||
139
agent/main.go
139
agent/main.go
@@ -2,7 +2,6 @@ package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
@@ -23,11 +22,21 @@ import (
|
||||
psutilNet "github.com/shirou/gopsutil/v4/net"
|
||||
)
|
||||
|
||||
var Version = "0.0.1-alpha.6"
|
||||
var Version = "0.1.0"
|
||||
|
||||
var containerCpuMap = make(map[string][2]uint64)
|
||||
var containerCpuMutex = &sync.Mutex{}
|
||||
|
||||
var sem = make(chan struct{}, 15)
|
||||
|
||||
func acquireSemaphore() {
|
||||
sem <- struct{}{}
|
||||
}
|
||||
|
||||
func releaseSemaphore() {
|
||||
<-sem
|
||||
}
|
||||
|
||||
var diskIoStats = DiskIoStats{
|
||||
Read: 0,
|
||||
Write: 0,
|
||||
@@ -44,59 +53,20 @@ var netIoStats = NetIoStats{
|
||||
|
||||
// client for docker engine api
|
||||
var client = &http.Client{
|
||||
Timeout: time.Second * 5,
|
||||
Timeout: time.Second,
|
||||
Transport: &http.Transport{
|
||||
Dial: func(proto, addr string) (net.Conn, error) {
|
||||
return net.Dial("unix", "/var/run/docker.sock")
|
||||
},
|
||||
ForceAttemptHTTP2: false,
|
||||
MaxIdleConns: 100,
|
||||
MaxIdleConnsPerHost: 100,
|
||||
IdleConnTimeout: 90 * time.Second,
|
||||
DisableCompression: true,
|
||||
MaxIdleConnsPerHost: 50,
|
||||
DisableKeepAlives: false,
|
||||
},
|
||||
}
|
||||
|
||||
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) {
|
||||
func getSystemStats() (*SystemInfo, *SystemStats) {
|
||||
c, _ := cpu.Percent(0, false)
|
||||
v, _ := mem.VirtualMemory()
|
||||
d, _ := disk.Usage("/")
|
||||
@@ -105,7 +75,7 @@ func getSystemStats() (SystemInfo, SystemStats) {
|
||||
memPct := twoDecimals(v.UsedPercent)
|
||||
diskPct := twoDecimals(d.UsedPercent)
|
||||
|
||||
systemStats := SystemStats{
|
||||
systemStats := &SystemStats{
|
||||
Cpu: cpuPct,
|
||||
Mem: bytesToGigabytes(v.Total),
|
||||
MemUsed: bytesToGigabytes(v.Used),
|
||||
@@ -116,7 +86,7 @@ func getSystemStats() (SystemInfo, SystemStats) {
|
||||
DiskPct: diskPct,
|
||||
}
|
||||
|
||||
systemInfo := SystemInfo{
|
||||
systemInfo := &SystemInfo{
|
||||
Cpu: cpuPct,
|
||||
MemPct: memPct,
|
||||
DiskPct: diskPct,
|
||||
@@ -182,54 +152,63 @@ func getSystemStats() (SystemInfo, SystemStats) {
|
||||
|
||||
}
|
||||
|
||||
func getDockerStats() ([]ContainerStats, error) {
|
||||
func getDockerStats() ([]*ContainerStats, error) {
|
||||
resp, err := client.Get("http://localhost/containers/json")
|
||||
if err != nil {
|
||||
return []ContainerStats{}, err
|
||||
return []*ContainerStats{}, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var containers []Container
|
||||
var containers []*Container
|
||||
if err := json.NewDecoder(resp.Body).Decode(&containers); err != nil {
|
||||
panic(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
|
||||
var containerStats []ContainerStats
|
||||
|
||||
for _, ctr := range containers {
|
||||
ctr.IdShort = ctr.ID[:12]
|
||||
validIds[ctr.IdShort] = struct{}{}
|
||||
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
|
||||
// retry once
|
||||
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()
|
||||
|
||||
for id := range containerCpuMap {
|
||||
if _, exists := validIds[id]; !exists {
|
||||
// log.Printf("Removing container cpu map entry: %+v\n", id)
|
||||
delete(containerCpuMap, id)
|
||||
}
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
return containerStats, nil
|
||||
}
|
||||
|
||||
func getContainerStats(ctr Container) (ContainerStats, error) {
|
||||
resp, err := client.Get("http://localhost/containers/" + ctr.ID + "/stats?stream=0&one-shot=1")
|
||||
func getContainerStats(ctr *Container) (*ContainerStats, error) {
|
||||
// use semaphore to limit concurrency
|
||||
acquireSemaphore()
|
||||
defer releaseSemaphore()
|
||||
resp, err := client.Get("http://localhost/containers/" + ctr.IdShort + "/stats?stream=0&one-shot=1")
|
||||
if err != nil {
|
||||
return ContainerStats{}, err
|
||||
return &ContainerStats{}, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
@@ -240,26 +219,30 @@ func getContainerStats(ctr Container) (ContainerStats, error) {
|
||||
|
||||
name := ctr.Names[0][1:]
|
||||
|
||||
// memory
|
||||
usedMemory := statsJson.MemoryStats.Usage - statsJson.MemoryStats.Cache
|
||||
// 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
|
||||
// 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}
|
||||
if _, ok := containerCpuMap[ctr.IdShort]; !ok {
|
||||
containerCpuMap[ctr.IdShort] = [2]uint64{0, 0}
|
||||
}
|
||||
cpuDelta := statsJson.CPUStats.CPUUsage.TotalUsage - containerCpuMap[name][0]
|
||||
systemDelta := statsJson.CPUStats.SystemUsage - containerCpuMap[name][1]
|
||||
cpuDelta := statsJson.CPUStats.CPUUsage.TotalUsage - containerCpuMap[ctr.IdShort][0]
|
||||
systemDelta := statsJson.CPUStats.SystemUsage - containerCpuMap[ctr.IdShort][1]
|
||||
cpuPct := float64(cpuDelta) / float64(systemDelta) * 100
|
||||
if cpuPct > 100 {
|
||||
return ContainerStats{}, errors.New("cpu pct is greater than 100")
|
||||
return &ContainerStats{}, fmt.Errorf("%s cpu pct greater than 100: %+v", name, cpuPct)
|
||||
}
|
||||
containerCpuMap[name] = [2]uint64{statsJson.CPUStats.CPUUsage.TotalUsage, statsJson.CPUStats.SystemUsage}
|
||||
containerCpuMap[ctr.IdShort] = [2]uint64{statsJson.CPUStats.CPUUsage.TotalUsage, statsJson.CPUStats.SystemUsage}
|
||||
|
||||
cStats := ContainerStats{
|
||||
cStats := &ContainerStats{
|
||||
Name: name,
|
||||
Cpu: twoDecimals(cpuPct),
|
||||
Mem: bytesToMegabytes(float64(usedMemory)),
|
||||
@@ -268,12 +251,12 @@ func getContainerStats(ctr Container) (ContainerStats, error) {
|
||||
return cStats, nil
|
||||
}
|
||||
|
||||
func gatherStats() SystemData {
|
||||
func gatherStats() *SystemData {
|
||||
systemInfo, systemStats := getSystemStats()
|
||||
stats := SystemData{
|
||||
stats := &SystemData{
|
||||
Stats: systemStats,
|
||||
Info: systemInfo,
|
||||
Containers: []ContainerStats{},
|
||||
Containers: []*ContainerStats{},
|
||||
}
|
||||
containerStats, err := getDockerStats()
|
||||
if err == nil {
|
||||
|
||||
@@ -2,8 +2,48 @@ package main
|
||||
|
||||
import "time"
|
||||
|
||||
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"`
|
||||
}
|
||||
|
||||
type Container struct {
|
||||
ID string `json:"Id"`
|
||||
IdShort string
|
||||
Names []string
|
||||
Image string
|
||||
ImageID string
|
||||
|
||||
50
hub/main.go
50
hub/main.go
@@ -15,24 +15,26 @@ import (
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/labstack/echo/v5"
|
||||
"github.com/pocketbase/dbx"
|
||||
"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"
|
||||
"github.com/pocketbase/pocketbase/tools/types"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
var Version = "0.0.1-alpha.6"
|
||||
var Version = "0.1.0"
|
||||
|
||||
var app *pocketbase.PocketBase
|
||||
var serverConnections = make(map[string]Server)
|
||||
var serverConnections = make(map[string]*Server)
|
||||
var serverConnectionsLock = sync.Mutex{}
|
||||
|
||||
func main() {
|
||||
app = pocketbase.NewWithConfig(pocketbase.Config{
|
||||
@@ -207,49 +209,53 @@ func startSystemUpdateTicker() {
|
||||
}
|
||||
|
||||
func updateSystems() {
|
||||
// handle max of 1/3 + 1 servers at a time
|
||||
numServers := len(serverConnections)/3 + 1
|
||||
// find systems that are not paused and updated more than 58 seconds ago
|
||||
fiftyEightSecondsAgo := time.Now().UTC().Add(-58 * time.Second).Format("2006-01-02 15:04:05")
|
||||
records, err := app.Dao().FindRecordsByFilter(
|
||||
"2hz5ncl8tizk5nx", // collection
|
||||
"status != 'paused' && updated < {:updated}", // filter
|
||||
"updated", // sort
|
||||
numServers, // limit
|
||||
0, // offset
|
||||
dbx.Params{"updated": fiftyEightSecondsAgo},
|
||||
"2hz5ncl8tizk5nx", // collection
|
||||
"status != 'paused'", // filter
|
||||
"updated", // sort
|
||||
-1, // limit
|
||||
0, // offset
|
||||
)
|
||||
if err != nil {
|
||||
app.Logger().Error("Failed to query systems: ", "err", err.Error())
|
||||
// log.Println("records", len(records))
|
||||
if err != nil || len(records) == 0 {
|
||||
// app.Logger().Error("Failed to query systems")
|
||||
return
|
||||
}
|
||||
for _, record := range records {
|
||||
updateSystem(record)
|
||||
fiftySecondsAgo := time.Now().UTC().Add(-50 * time.Second)
|
||||
batchSize := len(records)/4 + 1
|
||||
for i := 0; i < batchSize; i++ {
|
||||
if records[i].Get("updated").(types.DateTime).Time().After(fiftySecondsAgo) {
|
||||
break
|
||||
}
|
||||
// log.Println("updating", records[i].Get(("name")))
|
||||
go updateSystem(records[i])
|
||||
}
|
||||
}
|
||||
|
||||
func updateSystem(record *models.Record) {
|
||||
var server Server
|
||||
var server *Server
|
||||
// check if server connection data exists
|
||||
if _, ok := serverConnections[record.Id]; ok {
|
||||
server = serverConnections[record.Id]
|
||||
} else {
|
||||
// create server connection struct
|
||||
server = Server{
|
||||
server = &Server{
|
||||
Host: record.Get("host").(string),
|
||||
Port: record.Get("port").(string),
|
||||
}
|
||||
client, err := getServerConnection(&server)
|
||||
client, err := getServerConnection(server)
|
||||
if err != nil {
|
||||
app.Logger().Error("Failed to connect:", "err", err.Error(), "server", server.Host, "port", server.Port)
|
||||
updateServerStatus(record, "down")
|
||||
return
|
||||
}
|
||||
server.Client = client
|
||||
serverConnectionsLock.Lock()
|
||||
serverConnections[record.Id] = server
|
||||
serverConnectionsLock.Unlock()
|
||||
}
|
||||
// get server stats from agent
|
||||
systemData, err := requestJson(&server)
|
||||
systemData, err := requestJson(server)
|
||||
if err != nil {
|
||||
if err.Error() == "retry" {
|
||||
// if previous connection was closed, try again
|
||||
@@ -310,6 +316,8 @@ func deleteServerConnection(record *models.Record) {
|
||||
if serverConnections[record.Id].Client != nil {
|
||||
serverConnections[record.Id].Client.Close()
|
||||
}
|
||||
serverConnectionsLock.Lock()
|
||||
defer serverConnectionsLock.Unlock()
|
||||
delete(serverConnections, record.Id)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ export function AddSystemButton() {
|
||||
restart: unless-stopped
|
||||
network_mode: host
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
environment:
|
||||
PORT: ${port}
|
||||
KEY: "${publicKey}"
|
||||
|
||||
@@ -47,7 +47,13 @@ export default function BandwidthChart({
|
||||
<YAxis
|
||||
className="tracking-tighter"
|
||||
width={75}
|
||||
domain={[0, (max: number) => (max < 0.4 ? 0.4 : Math.ceil(max))]}
|
||||
domain={[0, (max: number) => (max <= 0.4 ? 0.4 : Math.ceil(max))]}
|
||||
tickFormatter={(value) => {
|
||||
if (value >= 100) {
|
||||
return value.toFixed(0)
|
||||
}
|
||||
return value.toFixed((value * 100) % 1 === 0 ? 1 : 2)
|
||||
}}
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
unit={' MB/s'}
|
||||
@@ -66,7 +72,8 @@ export default function BandwidthChart({
|
||||
tickFormatter={hourWithMinutes}
|
||||
/>
|
||||
<ChartTooltip
|
||||
// cursor={false}
|
||||
animationEasing="ease-out"
|
||||
animationDuration={150}
|
||||
content={
|
||||
<ChartTooltipContent
|
||||
unit=" MB/s"
|
||||
|
||||
@@ -69,7 +69,7 @@ export default function ContainerCpuChart({
|
||||
>
|
||||
<CartesianGrid vertical={false} />
|
||||
<YAxis
|
||||
domain={[0, (max: number) => Math.max(Math.ceil(max), 0.4)]}
|
||||
// domain={[0, (max: number) => Math.max(Math.ceil(max), 0.4)]}
|
||||
width={47}
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
|
||||
@@ -77,7 +77,8 @@ export default function DiskChart({
|
||||
tickFormatter={hourWithMinutes}
|
||||
/>
|
||||
<ChartTooltip
|
||||
// cursor={false}
|
||||
animationEasing="ease-out"
|
||||
animationDuration={150}
|
||||
content={
|
||||
<ChartTooltipContent
|
||||
unit=" GB"
|
||||
|
||||
@@ -47,7 +47,7 @@ export default function DiskIoChart({
|
||||
<YAxis
|
||||
className="tracking-tighter"
|
||||
width={75}
|
||||
domain={[0, (max: number) => (max < 0.4 ? 0.4 : Math.ceil(max))]}
|
||||
domain={[0, (max: number) => (max <= 0.4 ? 0.4 : Math.ceil(max))]}
|
||||
tickFormatter={(value) => {
|
||||
if (value >= 100) {
|
||||
return value.toFixed(0)
|
||||
@@ -72,7 +72,8 @@ export default function DiskIoChart({
|
||||
tickFormatter={hourWithMinutes}
|
||||
/>
|
||||
<ChartTooltip
|
||||
// cursor={false}
|
||||
animationEasing="ease-out"
|
||||
animationDuration={150}
|
||||
content={
|
||||
<ChartTooltipContent
|
||||
unit=" MB/s"
|
||||
|
||||
@@ -86,9 +86,12 @@ export default function ForgotPassword() {
|
||||
<DialogHeader>
|
||||
<DialogTitle>Command line instructions</DialogTitle>
|
||||
</DialogHeader>
|
||||
<p className="text-primary/70 text-[0.95em]">
|
||||
Use the following command to reset
|
||||
your password:
|
||||
<p className="text-primary/70 text-[0.95em] leading-relaxed">
|
||||
If you've lost the password to your admin account, you may reset it using the following
|
||||
command.
|
||||
</p>
|
||||
<p className="text-primary/70 text-[0.95em] leading-relaxed">
|
||||
Then log into the backend and reset your user account password in the users table.
|
||||
</p>
|
||||
<code className="bg-muted rounded-sm py-0.5 px-2.5 mr-auto text-sm">
|
||||
beszel admin update youremail@example.com newpassword
|
||||
|
||||
@@ -304,7 +304,7 @@ export default function SystemsTable() {
|
||||
? 'auto'
|
||||
: cell.column.getSize(),
|
||||
}}
|
||||
className={'overflow-hidden relative py-3'}
|
||||
className={'overflow-hidden relative py-2.5'}
|
||||
>
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</TableCell>
|
||||
|
||||
@@ -71,21 +71,21 @@ export default function AlertsButton({ system }: { system: SystemRecord }) {
|
||||
system={system}
|
||||
alerts={systemAlerts}
|
||||
name="CPU"
|
||||
title="CPU usage"
|
||||
title="CPU Usage"
|
||||
description="Triggers when CPU usage exceeds a threshold."
|
||||
/>
|
||||
<AlertWithSlider
|
||||
system={system}
|
||||
alerts={systemAlerts}
|
||||
name="Memory"
|
||||
title="Memory usage"
|
||||
title="Memory Usage"
|
||||
description="Triggers when memory usage exceeds a threshold."
|
||||
/>
|
||||
<AlertWithSlider
|
||||
system={system}
|
||||
alerts={systemAlerts}
|
||||
name="Disk"
|
||||
title="Disk usage"
|
||||
title="Disk Usage"
|
||||
description="Triggers when disk usage exceeds a threshold."
|
||||
/>
|
||||
</div>
|
||||
@@ -104,11 +104,11 @@ function AlertStatus({ system, alerts }: { system: SystemRecord; alerts: AlertRe
|
||||
return (
|
||||
<label
|
||||
htmlFor="alert-status"
|
||||
className="space-y-2 flex flex-row items-center justify-between rounded-lg border p-4 cursor-pointer"
|
||||
className="flex flex-row items-center justify-between gap-4 rounded-lg border p-4 cursor-pointer"
|
||||
>
|
||||
<div className="grid gap-0.5 select-none">
|
||||
<p className="font-medium text-[1.05em]">System status</p>
|
||||
<span className="block text-[0.85em] text-foreground opacity-80">
|
||||
<div className="grid gap-1 select-none">
|
||||
<p className="font-semibold">System Status</p>
|
||||
<span className="block text-sm text-foreground opacity-80">
|
||||
Triggers when status switches between up and down.
|
||||
</span>
|
||||
</div>
|
||||
@@ -171,13 +171,13 @@ function AlertWithSlider({
|
||||
<div className="rounded-lg border">
|
||||
<label
|
||||
htmlFor={`alert-${name}`}
|
||||
className={cn('space-y-2 flex flex-row items-center justify-between cursor-pointer p-4', {
|
||||
className={cn('flex flex-row items-center justify-between gap-4 cursor-pointer p-4', {
|
||||
'pb-0': !!alert,
|
||||
})}
|
||||
>
|
||||
<div className="grid gap-0.5 select-none">
|
||||
<p className="font-medium text-[1.05em]">{title}</p>
|
||||
<span className="block text-[0.85em] text-foreground opacity-80">{description}</span>
|
||||
<div className="grid gap-1 select-none">
|
||||
<p className="font-semibold">{title}</p>
|
||||
<span className="block text-sm text-foreground opacity-80">{description}</span>
|
||||
</div>
|
||||
<Switch
|
||||
id={`alert-${name}`}
|
||||
|
||||
@@ -20,12 +20,13 @@ export async function copyToClipboard(content: string) {
|
||||
description: 'Copied to clipboard',
|
||||
})
|
||||
} catch (e: any) {
|
||||
toast({
|
||||
duration,
|
||||
description: 'Failed to copy',
|
||||
})
|
||||
prompt(
|
||||
'Automatic copy requires a secure context (https, localhost, or *.localhost). Please copy manually:',
|
||||
content
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const verifyAuth = () => {
|
||||
pb.collection('users')
|
||||
.authRefresh()
|
||||
|
||||
67
readme.md
67
readme.md
@@ -1,28 +1,22 @@
|
||||
# Beszel \*WIP\*
|
||||
# Beszel
|
||||
|
||||
A lightweight server resource monitoring hub with historical data, docker stats, and alerts.
|
||||
|
||||
<!-- <table width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td width="50%"><img src="https://henrygd-assets.b-cdn.net/social-image-server/before-capture.png" alt="example of turso.tech/pricing link which is missing an og:image as of may 11 2024"/></td>
|
||||
<td width="50%"><img src="https://henrygd-assets.b-cdn.net/social-image-server/after-capture.webp" alt="example of turso.tech/pricing link using an image generated by the server as it's og:image"/></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table> -->
|
||||
[](https://hub.docker.com/r/henrygd/beszel-agent)
|
||||
[](https://hub.docker.com/r/henrygd/beszel)
|
||||
|
||||
<!-- ## Features
|
||||

|
||||
|
||||
## Features
|
||||
|
||||
- **Lightweight**: Much smaller and less demanding than leading solutions.
|
||||
- **Historical data**: Stats are available for up to 30 days.
|
||||
- **Docker stats**: CPU and memory usage history for each container.
|
||||
- **Alerts**: Lorem ipsum dolor sit amet, consectetur adipiscing elit.
|
||||
- **Simple**: Lorem ipsum dolor sit amet, consectetur adipiscing elit.
|
||||
- **Alerts**: Configurable alerts for CPU, memory, and disk usage, and system status.
|
||||
- **Multi-user**: Each user has their own systems. Admins can share systems across users.
|
||||
- **Secure**: Lorem ipsum dolor sit amet, consectetur adipiscing elit.
|
||||
- **Oauth / OIDC**: Supports many OAuth2 providers and password auth can be disabled.
|
||||
- **Automated backups**: Automatically back up your data to S3-compatible storage.
|
||||
- **Open source**: MIT license and no paywalled features. -->
|
||||
- **Simple**: Easy setup and doesn't require anything to be publicly available online.
|
||||
- **OAuth / OIDC**: Supports many OAuth2 providers. Password auth can be disabled.
|
||||
- **Automatic backups**: Save and restore your data to / from disk or S3-compatible storage.
|
||||
- **REST API**: Use your metrics in your own scripts and applications.
|
||||
|
||||
## Introduction
|
||||
|
||||
@@ -32,14 +26,11 @@ The hub is a web application that provides a dashboard to view and manage your c
|
||||
|
||||
The agent runs on each system you want to monitor. It creates a minimal SSH server through which it communicates system metrics to the hub.
|
||||
|
||||
[](https://hub.docker.com/r/henrygd/beszel-agent)
|
||||
[](https://hub.docker.com/r/henrygd/beszel)
|
||||
|
||||
## Getting started
|
||||
|
||||
If using the binary instead of docker, ignore 4-5 and run the agent using the binary instead.
|
||||
|
||||
1. Start the hub (see [Installation](#installation)). The binary command is `beszel serve`.
|
||||
1. Start the hub (see [installation](#installation)). The binary command is `beszel serve`.
|
||||
2. Open http://localhost:8090 and create an admin user.
|
||||
3. Click "Add system." Enter the name and host of the system you want to monitor.
|
||||
4. Click "Copy docker compose" to copy the agent's docker-compose.yml file to your clipboard.
|
||||
@@ -104,8 +95,9 @@ Use `beszel update` and `beszel-agent update` to update to the latest version.
|
||||
|
||||
Beszel supports OpenID Connect and many OAuth2 authentication providers (see list below). To enable, do the following:
|
||||
|
||||
1. Create an OAuth2 application using your provider of choice. The redirect / callback URL should be `<your-beszel-url>/api/oauth2-redirect`.
|
||||
2. When you have the client ID and secret, go to the "Auth providers" page and enable your provider.
|
||||
1. Make sure your "Application URL" is set correctly in the PocketBase settings.
|
||||
2. Create an OAuth2 application using your provider of choice. The redirect / callback URL should be `<your-beszel-url>/api/oauth2-redirect`.
|
||||
3. When you have the client ID and secret, go to the "Auth providers" page and enable your provider.
|
||||
|
||||
<details>
|
||||
<summary>Supported provider list</summary>
|
||||
@@ -144,7 +136,25 @@ The hub and agent communicate over SSH, so they don't need to be exposed to the
|
||||
|
||||
When the hub is started for the first time, it generates an ED25519 key pair.
|
||||
|
||||
The agent's SSH server is configured to accept connections only using this key. It does not provide a pty or accept any input, so it is not possible to execute commands on the agent even if your private key is compromised.
|
||||
The agent's SSH server is configured to accept connections only using this key. It does not provide a pseudo-terminal or accept input, so it's not possible to execute commands on the agent even if your private key is compromised.
|
||||
|
||||
## User roles
|
||||
|
||||
### Admin
|
||||
|
||||
Assumed to have an admin account in PocketBase, so links to backups, SMTP settings, etc., are shown in the hub.
|
||||
|
||||
The first user created automatically becomes an admin and can log into PocketBase.
|
||||
|
||||
Please note that changing a user's role will not create a PocketBase admin account for them. If you want to do that, go to Settings > Admins in PocketBase and add them there.
|
||||
|
||||
### User
|
||||
|
||||
Can create their own systems and alerts. Links to PocketBase settings are not shown in the hub.
|
||||
|
||||
### Read only
|
||||
|
||||
Cannot create systems, but can view any system that has been shared with them by an admin. Can create alerts.
|
||||
|
||||
## FAQ / Troubleshooting
|
||||
|
||||
@@ -164,8 +174,17 @@ If it's not set, the agent will try to find the filesystem mounted on `/` and us
|
||||
- Run `lsblk` and choose an option under "NAME"
|
||||
- Run `sudo fdisk -l` and choose an option under "Device"
|
||||
|
||||
### Docker containers are not populating reliably
|
||||
|
||||
Try upgrading your docker version on the agent system. I had this issue on a machine running version 24. It was fixed by upgrading to version 27.
|
||||
|
||||
### Month / week records are not populating reliably
|
||||
|
||||
Records for longer time periods are made by averaging stats from the shorter time periods. They require the agent to be running uninterrupted for long enough to get a full set of data.
|
||||
|
||||
If you pause / unpause the agent for longer than one minute, the data will be incomplete and the timing for the current interval will reset.
|
||||
|
||||
<!--
|
||||
## Support
|
||||
|
||||
My country, the USA, and many others, are actively involved in the genocide of the Palestinian people. I would greatly appreciate any effort you could make to pressure your government to stop enabling this violence. -->
|
||||
|
||||
Reference in New Issue
Block a user