mirror of
https://github.com/henrygd/beszel.git
synced 2026-03-22 05:36:15 +01:00
Compare commits
9 Commits
v0.0.1-alp
...
v0.0.1-alp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ae0f5c938f | ||
|
|
78dc269538 | ||
|
|
f6967eab35 | ||
|
|
e787b6ea1b | ||
|
|
844b95dfd0 | ||
|
|
c5776541a0 | ||
|
|
5ba7568acf | ||
|
|
14c7e2db8f | ||
|
|
51ed130b53 |
2
.github/workflows/docker-images.yml
vendored
2
.github/workflows/docker-images.yml
vendored
@@ -30,7 +30,7 @@ jobs:
|
||||
uses: oven-sh/setup-bun@v2
|
||||
|
||||
- name: Install dependencies
|
||||
run: bun install --cwd ./hub/site
|
||||
run: bun install --no-save --cwd ./hub/site
|
||||
|
||||
- name: Build site
|
||||
run: bun run --cwd ./hub/site build
|
||||
|
||||
2
.github/workflows/goreleaser-action.yml
vendored
2
.github/workflows/goreleaser-action.yml
vendored
@@ -21,7 +21,7 @@ jobs:
|
||||
uses: oven-sh/setup-bun@v2
|
||||
|
||||
- name: Install dependencies
|
||||
run: bun install --cwd ./hub/site
|
||||
run: bun install --no-save --cwd ./hub/site
|
||||
|
||||
- name: Build site
|
||||
run: bun run --cwd ./hub/site build
|
||||
|
||||
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'
|
||||
|
||||
@@ -2,7 +2,6 @@ package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
@@ -11,7 +10,6 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
sshServer "github.com/gliderlabs/ssh"
|
||||
@@ -23,10 +21,11 @@ import (
|
||||
psutilNet "github.com/shirou/gopsutil/v4/net"
|
||||
)
|
||||
|
||||
var Version = "0.0.1-alpha.5"
|
||||
var Version = "0.0.1-alpha.7"
|
||||
|
||||
var containerCpuMap = make(map[string][2]uint64)
|
||||
var containerCpuMutex = &sync.Mutex{}
|
||||
|
||||
// var containerCpuMutex = &sync.Mutex{}
|
||||
|
||||
var diskIoStats = DiskIoStats{
|
||||
Read: 0,
|
||||
@@ -44,16 +43,16 @@ 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,
|
||||
DisableKeepAlives: false,
|
||||
ForceAttemptHTTP2: false,
|
||||
IdleConnTimeout: 90 * time.Second,
|
||||
DisableCompression: true,
|
||||
MaxIdleConns: 10,
|
||||
DisableKeepAlives: false,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -194,20 +193,19 @@ func getDockerStats() ([]ContainerStats, error) {
|
||||
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)
|
||||
cstats, err := getContainerStats(ctr)
|
||||
if err != nil {
|
||||
// retry once
|
||||
cstats, err = getContainerStats(ctr)
|
||||
if err != nil {
|
||||
log.Printf("Error getting container stats: %+v\n", err)
|
||||
return
|
||||
continue
|
||||
}
|
||||
containerStats = append(containerStats, cstats)
|
||||
}()
|
||||
}
|
||||
containerStats = append(containerStats, cstats)
|
||||
}
|
||||
|
||||
// clean up old containers from map
|
||||
@@ -221,8 +219,6 @@ func getDockerStats() ([]ContainerStats, error) {
|
||||
}
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
return containerStats, nil
|
||||
}
|
||||
|
||||
@@ -246,8 +242,8 @@ func getContainerStats(ctr Container) (ContainerStats, error) {
|
||||
|
||||
// cpu
|
||||
// add default values to containerCpu if it doesn't exist
|
||||
containerCpuMutex.Lock()
|
||||
defer containerCpuMutex.Unlock()
|
||||
// containerCpuMutex.Lock()
|
||||
// defer containerCpuMutex.Unlock()
|
||||
if _, ok := containerCpuMap[name]; !ok {
|
||||
containerCpuMap[name] = [2]uint64{0, 0}
|
||||
}
|
||||
@@ -255,7 +251,7 @@ func getContainerStats(ctr Container) (ContainerStats, error) {
|
||||
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")
|
||||
return ContainerStats{}, fmt.Errorf("%s cpu pct greater than 100: %+v", name, cpuPct)
|
||||
}
|
||||
containerCpuMap[name] = [2]uint64{statsJson.CPUStats.CPUUsage.TotalUsage, statsJson.CPUStats.SystemUsage}
|
||||
|
||||
|
||||
@@ -31,4 +31,4 @@ COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
|
||||
EXPOSE 8090
|
||||
|
||||
ENTRYPOINT [ "/beszel" ]
|
||||
CMD ["serve", "--http=localhost:8090"]
|
||||
CMD ["serve", "--http=0.0.0.0:8090"]
|
||||
46
hub/main.go
46
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.5"
|
||||
var Version = "0.0.1-alpha.7"
|
||||
|
||||
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
|
||||
)
|
||||
// log.Println("records", len(records))
|
||||
if err != nil {
|
||||
app.Logger().Error("Failed to query systems: ", "err", err.Error())
|
||||
return
|
||||
}
|
||||
for _, record := range records {
|
||||
updateSystem(record)
|
||||
fiftyFiveSecondsAgo := time.Now().UTC().Add(-55 * time.Second)
|
||||
batchSize := len(records)/4 + 1
|
||||
for i := 0; i < batchSize; i++ {
|
||||
if records[i].Get("updated").(types.DateTime).Time().After(fiftyFiveSecondsAgo) {
|
||||
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'}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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}`}
|
||||
|
||||
36
readme.md
36
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.
|
||||
- **Automated backups**: Automatically back up your data to disk or S3-compatible storage.
|
||||
- **REST API**: Use your metrics in your own scripts and applications.
|
||||
|
||||
## Introduction
|
||||
|
||||
@@ -32,9 +26,6 @@ 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.
|
||||
@@ -169,3 +160,8 @@ If it's not set, the agent will try to find the filesystem mounted on `/` and us
|
||||
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