mirror of
https://github.com/henrygd/beszel.git
synced 2026-03-23 14:06:18 +01:00
Compare commits
8 Commits
v0.0.1-alp
...
v0.1.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
26dbb1968a | ||
|
|
ee57e84cb8 | ||
|
|
345dbeb757 | ||
|
|
29f5d3ae62 | ||
|
|
d4b0887153 | ||
|
|
06e4dd10e0 | ||
|
|
af4d5137d6 | ||
|
|
5e255f8f69 |
@@ -10,6 +10,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
sshServer "github.com/gliderlabs/ssh"
|
sshServer "github.com/gliderlabs/ssh"
|
||||||
@@ -21,11 +22,20 @@ import (
|
|||||||
psutilNet "github.com/shirou/gopsutil/v4/net"
|
psutilNet "github.com/shirou/gopsutil/v4/net"
|
||||||
)
|
)
|
||||||
|
|
||||||
var Version = "0.0.1-alpha.8"
|
var Version = "0.1.0"
|
||||||
|
|
||||||
var containerCpuMap = make(map[string][2]uint64)
|
var containerCpuMap = make(map[string][2]uint64)
|
||||||
|
var containerCpuMutex = &sync.Mutex{}
|
||||||
|
|
||||||
// var containerCpuMutex = &sync.Mutex{}
|
var sem = make(chan struct{}, 15)
|
||||||
|
|
||||||
|
func acquireSemaphore() {
|
||||||
|
sem <- struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func releaseSemaphore() {
|
||||||
|
<-sem
|
||||||
|
}
|
||||||
|
|
||||||
var diskIoStats = DiskIoStats{
|
var diskIoStats = DiskIoStats{
|
||||||
Read: 0,
|
Read: 0,
|
||||||
@@ -48,11 +58,11 @@ var client = &http.Client{
|
|||||||
Dial: func(proto, addr string) (net.Conn, error) {
|
Dial: func(proto, addr string) (net.Conn, error) {
|
||||||
return net.Dial("unix", "/var/run/docker.sock")
|
return net.Dial("unix", "/var/run/docker.sock")
|
||||||
},
|
},
|
||||||
ForceAttemptHTTP2: false,
|
ForceAttemptHTTP2: false,
|
||||||
IdleConnTimeout: 90 * time.Second,
|
IdleConnTimeout: 90 * time.Second,
|
||||||
DisableCompression: true,
|
DisableCompression: true,
|
||||||
MaxIdleConns: 10,
|
MaxIdleConnsPerHost: 50,
|
||||||
DisableKeepAlives: false,
|
DisableKeepAlives: false,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -156,27 +166,35 @@ func getDockerStats() ([]*ContainerStats, error) {
|
|||||||
|
|
||||||
containerStats := make([]*ContainerStats, 0, len(containers))
|
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 {
|
for _, ctr := range containers {
|
||||||
ctr.IdShort = ctr.ID[:12]
|
ctr.IdShort = ctr.ID[:12]
|
||||||
cstats, err := getContainerStats(ctr)
|
validIds[ctr.IdShort] = struct{}{}
|
||||||
if err != nil {
|
wg.Add(1)
|
||||||
// retry once
|
go func() {
|
||||||
cstats, err = getContainerStats(ctr)
|
defer wg.Done()
|
||||||
|
cstats, err := getContainerStats(ctr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Error getting container stats: %+v\n", err)
|
// retry once
|
||||||
continue
|
cstats, err = getContainerStats(ctr)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error getting container stats: %+v\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
containerStats = append(containerStats, cstats)
|
||||||
containerStats = append(containerStats, cstats)
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
// clean up old container ids from map
|
wg.Wait()
|
||||||
validIds := make(map[string]struct{}, len(containers))
|
|
||||||
for _, ctr := range containers {
|
|
||||||
validIds[ctr.IdShort] = struct{}{}
|
|
||||||
}
|
|
||||||
for id := range containerCpuMap {
|
for id := range containerCpuMap {
|
||||||
if _, exists := validIds[id]; !exists {
|
if _, exists := validIds[id]; !exists {
|
||||||
|
// log.Printf("Removing container cpu map entry: %+v\n", id)
|
||||||
delete(containerCpuMap, id)
|
delete(containerCpuMap, id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -185,6 +203,9 @@ func getDockerStats() ([]*ContainerStats, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func getContainerStats(ctr *Container) (*ContainerStats, error) {
|
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")
|
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
|
||||||
@@ -198,14 +219,18 @@ func getContainerStats(ctr *Container) (*ContainerStats, error) {
|
|||||||
|
|
||||||
name := ctr.Names[0][1:]
|
name := ctr.Names[0][1:]
|
||||||
|
|
||||||
// memory
|
// memory (https://docs.docker.com/reference/cli/docker/container/stats/)
|
||||||
usedMemory := statsJson.MemoryStats.Usage - statsJson.MemoryStats.Cache
|
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
|
// pctMemory := float64(usedMemory) / float64(statsJson.MemoryStats.Limit) * 100
|
||||||
|
|
||||||
// cpu
|
// cpu
|
||||||
// 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[ctr.IdShort]; !ok {
|
if _, ok := containerCpuMap[ctr.IdShort]; !ok {
|
||||||
containerCpuMap[ctr.IdShort] = [2]uint64{0, 0}
|
containerCpuMap[ctr.IdShort] = [2]uint64{0, 0}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ import (
|
|||||||
"golang.org/x/crypto/ssh"
|
"golang.org/x/crypto/ssh"
|
||||||
)
|
)
|
||||||
|
|
||||||
var Version = "0.0.1-alpha.8"
|
var Version = "0.1.0"
|
||||||
|
|
||||||
var app *pocketbase.PocketBase
|
var app *pocketbase.PocketBase
|
||||||
var serverConnections = make(map[string]*Server)
|
var serverConnections = make(map[string]*Server)
|
||||||
@@ -221,10 +221,10 @@ func updateSystems() {
|
|||||||
// app.Logger().Error("Failed to query systems")
|
// app.Logger().Error("Failed to query systems")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
fiftyFiveSecondsAgo := time.Now().UTC().Add(-55 * time.Second)
|
fiftySecondsAgo := time.Now().UTC().Add(-50 * time.Second)
|
||||||
batchSize := len(records)/4 + 1
|
batchSize := len(records)/4 + 1
|
||||||
for i := 0; i < batchSize; i++ {
|
for i := 0; i < batchSize; i++ {
|
||||||
if records[i].Get("updated").(types.DateTime).Time().After(fiftyFiveSecondsAgo) {
|
if records[i].Get("updated").(types.DateTime).Time().After(fiftySecondsAgo) {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
// log.Println("updating", records[i].Get(("name")))
|
// log.Println("updating", records[i].Get(("name")))
|
||||||
|
|||||||
@@ -86,9 +86,12 @@ export default function ForgotPassword() {
|
|||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Command line instructions</DialogTitle>
|
<DialogTitle>Command line instructions</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<p className="text-primary/70 text-[0.95em]">
|
<p className="text-primary/70 text-[0.95em] leading-relaxed">
|
||||||
Use the following command to reset
|
If you've lost the password to your admin account, you may reset it using the following
|
||||||
your password:
|
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>
|
</p>
|
||||||
<code className="bg-muted rounded-sm py-0.5 px-2.5 mr-auto text-sm">
|
<code className="bg-muted rounded-sm py-0.5 px-2.5 mr-auto text-sm">
|
||||||
beszel admin update youremail@example.com newpassword
|
beszel admin update youremail@example.com newpassword
|
||||||
|
|||||||
@@ -20,12 +20,13 @@ export async function copyToClipboard(content: string) {
|
|||||||
description: 'Copied to clipboard',
|
description: 'Copied to clipboard',
|
||||||
})
|
})
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
toast({
|
prompt(
|
||||||
duration,
|
'Automatic copy requires a secure context (https, localhost, or *.localhost). Please copy manually:',
|
||||||
description: 'Failed to copy',
|
content
|
||||||
})
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const verifyAuth = () => {
|
const verifyAuth = () => {
|
||||||
pb.collection('users')
|
pb.collection('users')
|
||||||
.authRefresh()
|
.authRefresh()
|
||||||
|
|||||||
12
readme.md
12
readme.md
@@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
A lightweight server resource monitoring hub with historical data, docker stats, and alerts.
|
A lightweight server resource monitoring hub with historical data, docker stats, and alerts.
|
||||||
|
|
||||||
[](https://hub.docker.com/r/henrygd/beszel-agent)
|
[](https://hub.docker.com/r/henrygd/beszel-agent)
|
||||||
[](https://hub.docker.com/r/henrygd/beszel)
|
[](https://hub.docker.com/r/henrygd/beszel)
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@@ -15,7 +15,7 @@ A lightweight server resource monitoring hub with historical data, docker stats,
|
|||||||
- **Multi-user**: Each user has their own systems. Admins can share systems across users.
|
- **Multi-user**: Each user has their own systems. Admins can share systems across users.
|
||||||
- **Simple**: Easy setup and doesn't require anything to be publicly available online.
|
- **Simple**: Easy setup and doesn't require anything to be publicly available online.
|
||||||
- **OAuth / OIDC**: Supports many OAuth2 providers. Password auth can be disabled.
|
- **OAuth / OIDC**: Supports many OAuth2 providers. Password auth can be disabled.
|
||||||
- **Automated backups**: Automatically back up your data to disk or S3-compatible storage.
|
- **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.
|
- **REST API**: Use your metrics in your own scripts and applications.
|
||||||
|
|
||||||
## Introduction
|
## Introduction
|
||||||
@@ -30,7 +30,7 @@ The agent runs on each system you want to monitor. It creates a minimal SSH serv
|
|||||||
|
|
||||||
If using the binary instead of docker, ignore 4-5 and run the agent using the binary instead.
|
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.
|
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.
|
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.
|
4. Click "Copy docker compose" to copy the agent's docker-compose.yml file to your clipboard.
|
||||||
@@ -136,7 +136,7 @@ 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.
|
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
|
## User roles
|
||||||
|
|
||||||
@@ -176,7 +176,7 @@ If it's not set, the agent will try to find the filesystem mounted on `/` and us
|
|||||||
|
|
||||||
### Docker containers are not populating reliably
|
### 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.
|
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
|
### Month / week records are not populating reliably
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user