mirror of
https://github.com/henrygd/beszel.git
synced 2026-03-24 06:26:17 +01:00
Compare commits
5 Commits
v0.0.1-alp
...
v0.0.1-alp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
76cfaaa179 | ||
|
|
b89bec31b5 | ||
|
|
0355d9c654 | ||
|
|
41df7b7392 | ||
|
|
52c77dd361 |
@@ -21,7 +21,7 @@ import (
|
|||||||
psutilNet "github.com/shirou/gopsutil/v4/net"
|
psutilNet "github.com/shirou/gopsutil/v4/net"
|
||||||
)
|
)
|
||||||
|
|
||||||
var Version = "0.0.1-alpha.7"
|
var Version = "0.0.1-alpha.8"
|
||||||
|
|
||||||
var containerCpuMap = make(map[string][2]uint64)
|
var containerCpuMap = make(map[string][2]uint64)
|
||||||
|
|
||||||
@@ -56,46 +56,7 @@ var client = &http.Client{
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
type SystemData struct {
|
func getSystemStats() (*SystemInfo, *SystemStats) {
|
||||||
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)
|
c, _ := cpu.Percent(0, false)
|
||||||
v, _ := mem.VirtualMemory()
|
v, _ := mem.VirtualMemory()
|
||||||
d, _ := disk.Usage("/")
|
d, _ := disk.Usage("/")
|
||||||
@@ -104,7 +65,7 @@ func getSystemStats() (SystemInfo, SystemStats) {
|
|||||||
memPct := twoDecimals(v.UsedPercent)
|
memPct := twoDecimals(v.UsedPercent)
|
||||||
diskPct := twoDecimals(d.UsedPercent)
|
diskPct := twoDecimals(d.UsedPercent)
|
||||||
|
|
||||||
systemStats := SystemStats{
|
systemStats := &SystemStats{
|
||||||
Cpu: cpuPct,
|
Cpu: cpuPct,
|
||||||
Mem: bytesToGigabytes(v.Total),
|
Mem: bytesToGigabytes(v.Total),
|
||||||
MemUsed: bytesToGigabytes(v.Used),
|
MemUsed: bytesToGigabytes(v.Used),
|
||||||
@@ -115,7 +76,7 @@ func getSystemStats() (SystemInfo, SystemStats) {
|
|||||||
DiskPct: diskPct,
|
DiskPct: diskPct,
|
||||||
}
|
}
|
||||||
|
|
||||||
systemInfo := SystemInfo{
|
systemInfo := &SystemInfo{
|
||||||
Cpu: cpuPct,
|
Cpu: cpuPct,
|
||||||
MemPct: memPct,
|
MemPct: memPct,
|
||||||
DiskPct: diskPct,
|
DiskPct: diskPct,
|
||||||
@@ -181,21 +142,22 @@ func getSystemStats() (SystemInfo, SystemStats) {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func getDockerStats() ([]ContainerStats, error) {
|
func getDockerStats() ([]*ContainerStats, error) {
|
||||||
resp, err := client.Get("http://localhost/containers/json")
|
resp, err := client.Get("http://localhost/containers/json")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return []ContainerStats{}, err
|
return []*ContainerStats{}, err
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
var containers []Container
|
var containers []*Container
|
||||||
if err := json.NewDecoder(resp.Body).Decode(&containers); err != nil {
|
if err := json.NewDecoder(resp.Body).Decode(&containers); err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var containerStats []ContainerStats
|
containerStats := make([]*ContainerStats, 0, len(containers))
|
||||||
|
|
||||||
for _, ctr := range containers {
|
for _, ctr := range containers {
|
||||||
|
ctr.IdShort = ctr.ID[:12]
|
||||||
cstats, err := getContainerStats(ctr)
|
cstats, err := getContainerStats(ctr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// retry once
|
// retry once
|
||||||
@@ -208,24 +170,24 @@ func getDockerStats() ([]ContainerStats, error) {
|
|||||||
containerStats = append(containerStats, cstats)
|
containerStats = append(containerStats, cstats)
|
||||||
}
|
}
|
||||||
|
|
||||||
// clean up old containers from map
|
// clean up old container ids from map
|
||||||
validNames := make(map[string]struct{}, len(containers))
|
validIds := make(map[string]struct{}, len(containers))
|
||||||
for _, ctr := range containers {
|
for _, ctr := range containers {
|
||||||
validNames[ctr.Names[0][1:]] = struct{}{}
|
validIds[ctr.IdShort] = struct{}{}
|
||||||
}
|
}
|
||||||
for name := range containerCpuMap {
|
for id := range containerCpuMap {
|
||||||
if _, exists := validNames[name]; !exists {
|
if _, exists := validIds[id]; !exists {
|
||||||
delete(containerCpuMap, name)
|
delete(containerCpuMap, id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return containerStats, nil
|
return containerStats, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getContainerStats(ctr Container) (ContainerStats, error) {
|
func getContainerStats(ctr *Container) (*ContainerStats, error) {
|
||||||
resp, err := client.Get("http://localhost/containers/" + ctr.ID + "/stats?stream=0&one-shot=1")
|
resp, err := client.Get("http://localhost/containers/" + ctr.IdShort + "/stats?stream=0&one-shot=1")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ContainerStats{}, err
|
return &ContainerStats{}, err
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
@@ -244,18 +206,18 @@ func getContainerStats(ctr Container) (ContainerStats, error) {
|
|||||||
// add default values to containerCpu if it doesn't exist
|
// add default values to containerCpu if it doesn't exist
|
||||||
// containerCpuMutex.Lock()
|
// containerCpuMutex.Lock()
|
||||||
// defer containerCpuMutex.Unlock()
|
// defer containerCpuMutex.Unlock()
|
||||||
if _, ok := containerCpuMap[name]; !ok {
|
if _, ok := containerCpuMap[ctr.IdShort]; !ok {
|
||||||
containerCpuMap[name] = [2]uint64{0, 0}
|
containerCpuMap[ctr.IdShort] = [2]uint64{0, 0}
|
||||||
}
|
}
|
||||||
cpuDelta := statsJson.CPUStats.CPUUsage.TotalUsage - containerCpuMap[name][0]
|
cpuDelta := statsJson.CPUStats.CPUUsage.TotalUsage - containerCpuMap[ctr.IdShort][0]
|
||||||
systemDelta := statsJson.CPUStats.SystemUsage - containerCpuMap[name][1]
|
systemDelta := statsJson.CPUStats.SystemUsage - containerCpuMap[ctr.IdShort][1]
|
||||||
cpuPct := float64(cpuDelta) / float64(systemDelta) * 100
|
cpuPct := float64(cpuDelta) / float64(systemDelta) * 100
|
||||||
if cpuPct > 100 {
|
if cpuPct > 100 {
|
||||||
return ContainerStats{}, fmt.Errorf("%s cpu pct greater than 100: %+v", name, cpuPct)
|
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,
|
Name: name,
|
||||||
Cpu: twoDecimals(cpuPct),
|
Cpu: twoDecimals(cpuPct),
|
||||||
Mem: bytesToMegabytes(float64(usedMemory)),
|
Mem: bytesToMegabytes(float64(usedMemory)),
|
||||||
@@ -264,12 +226,12 @@ func getContainerStats(ctr Container) (ContainerStats, error) {
|
|||||||
return cStats, nil
|
return cStats, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func gatherStats() SystemData {
|
func gatherStats() *SystemData {
|
||||||
systemInfo, systemStats := getSystemStats()
|
systemInfo, systemStats := getSystemStats()
|
||||||
stats := SystemData{
|
stats := &SystemData{
|
||||||
Stats: systemStats,
|
Stats: systemStats,
|
||||||
Info: systemInfo,
|
Info: systemInfo,
|
||||||
Containers: []ContainerStats{},
|
Containers: []*ContainerStats{},
|
||||||
}
|
}
|
||||||
containerStats, err := getDockerStats()
|
containerStats, err := getDockerStats()
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
|||||||
@@ -2,8 +2,48 @@ package main
|
|||||||
|
|
||||||
import "time"
|
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 {
|
type Container struct {
|
||||||
ID string `json:"Id"`
|
ID string `json:"Id"`
|
||||||
|
IdShort string
|
||||||
Names []string
|
Names []string
|
||||||
Image string
|
Image string
|
||||||
ImageID string
|
ImageID string
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ import (
|
|||||||
"golang.org/x/crypto/ssh"
|
"golang.org/x/crypto/ssh"
|
||||||
)
|
)
|
||||||
|
|
||||||
var Version = "0.0.1-alpha.7"
|
var Version = "0.0.1-alpha.8"
|
||||||
|
|
||||||
var app *pocketbase.PocketBase
|
var app *pocketbase.PocketBase
|
||||||
var serverConnections = make(map[string]*Server)
|
var serverConnections = make(map[string]*Server)
|
||||||
@@ -217,8 +217,8 @@ func updateSystems() {
|
|||||||
0, // offset
|
0, // offset
|
||||||
)
|
)
|
||||||
// log.Println("records", len(records))
|
// log.Println("records", len(records))
|
||||||
if err != nil {
|
if err != nil || len(records) == 0 {
|
||||||
app.Logger().Error("Failed to query systems: ", "err", err.Error())
|
// app.Logger().Error("Failed to query systems")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
fiftyFiveSecondsAgo := time.Now().UTC().Add(-55 * time.Second)
|
fiftyFiveSecondsAgo := time.Now().UTC().Add(-55 * time.Second)
|
||||||
|
|||||||
@@ -72,7 +72,8 @@ export default function BandwidthChart({
|
|||||||
tickFormatter={hourWithMinutes}
|
tickFormatter={hourWithMinutes}
|
||||||
/>
|
/>
|
||||||
<ChartTooltip
|
<ChartTooltip
|
||||||
// cursor={false}
|
animationEasing="ease-out"
|
||||||
|
animationDuration={150}
|
||||||
content={
|
content={
|
||||||
<ChartTooltipContent
|
<ChartTooltipContent
|
||||||
unit=" MB/s"
|
unit=" MB/s"
|
||||||
|
|||||||
@@ -77,7 +77,8 @@ export default function DiskChart({
|
|||||||
tickFormatter={hourWithMinutes}
|
tickFormatter={hourWithMinutes}
|
||||||
/>
|
/>
|
||||||
<ChartTooltip
|
<ChartTooltip
|
||||||
// cursor={false}
|
animationEasing="ease-out"
|
||||||
|
animationDuration={150}
|
||||||
content={
|
content={
|
||||||
<ChartTooltipContent
|
<ChartTooltipContent
|
||||||
unit=" GB"
|
unit=" GB"
|
||||||
|
|||||||
@@ -72,7 +72,8 @@ export default function DiskIoChart({
|
|||||||
tickFormatter={hourWithMinutes}
|
tickFormatter={hourWithMinutes}
|
||||||
/>
|
/>
|
||||||
<ChartTooltip
|
<ChartTooltip
|
||||||
// cursor={false}
|
animationEasing="ease-out"
|
||||||
|
animationDuration={150}
|
||||||
content={
|
content={
|
||||||
<ChartTooltipContent
|
<ChartTooltipContent
|
||||||
unit=" MB/s"
|
unit=" MB/s"
|
||||||
|
|||||||
@@ -304,7 +304,7 @@ export default function SystemsTable() {
|
|||||||
? 'auto'
|
? 'auto'
|
||||||
: cell.column.getSize(),
|
: cell.column.getSize(),
|
||||||
}}
|
}}
|
||||||
className={'overflow-hidden relative py-3'}
|
className={'overflow-hidden relative py-2.5'}
|
||||||
>
|
>
|
||||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|||||||
27
readme.md
27
readme.md
@@ -95,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:
|
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`.
|
1. Make sure your "Application URL" is set correctly in the PocketBase settings.
|
||||||
2. When you have the client ID and secret, go to the "Auth providers" page and enable your provider.
|
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>
|
<details>
|
||||||
<summary>Supported provider list</summary>
|
<summary>Supported provider list</summary>
|
||||||
@@ -137,6 +138,24 @@ 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 pty or accept any input, so it is 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
|
## FAQ / Troubleshooting
|
||||||
|
|
||||||
### Agent is not connecting
|
### Agent is not connecting
|
||||||
@@ -155,6 +174,10 @@ 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 `lsblk` and choose an option under "NAME"
|
||||||
- Run `sudo fdisk -l` and choose an option under "Device"
|
- 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 docker 24. It was fixed by upgrading to version 27.
|
||||||
|
|
||||||
### Month / week records are not populating reliably
|
### 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.
|
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.
|
||||||
|
|||||||
Reference in New Issue
Block a user