diff --git a/agent/types.go b/agent/types.go deleted file mode 100644 index 1d48eb0..0000000 --- a/agent/types.go +++ /dev/null @@ -1,177 +0,0 @@ -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"` - Swap float64 `json:"s"` - SwapUsed float64 `json:"su"` - 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"` - NetworkSent float64 `json:"ns"` - NetworkRecv float64 `json:"nr"` -} - -type Container struct { - Id string - IdShort string - Names []string - Status 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 - // 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"` - // Networks request version >=1.21 - Networks map[string]NetworkStats - - // 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 NetworkStats struct { - // Bytes received. Windows and Linux. - RxBytes uint64 `json:"rx_bytes"` - // Bytes sent. Windows and Linux. - TxBytes uint64 `json:"tx_bytes"` -} - -type DiskIoStats struct { - Read uint64 - Write uint64 - Time time.Time - Filesystem string -} - -type NetIoStats struct { - BytesRecv uint64 - BytesSent uint64 - Time time.Time - Name string -} - -type PrevContainerStats struct { - Cpu [2]uint64 - Net struct { - Sent uint64 - Recv uint64 - Time time.Time - } -} diff --git a/agent/update.go b/agent/update.go deleted file mode 100644 index fef463a..0000000 --- a/agent/update.go +++ /dev/null @@ -1,54 +0,0 @@ -package main - -import ( - "fmt" - "os" - "strings" - - "github.com/blang/semver" - "github.com/rhysd/go-github-selfupdate/selfupdate" -) - -func updateBeszel() { - var latest *selfupdate.Release - var found bool - var err error - currentVersion := semver.MustParse(Version) - fmt.Println("beszel-agent", currentVersion) - fmt.Println("Checking for updates...") - updater, _ := selfupdate.NewUpdater(selfupdate.Config{ - Filters: []string{"beszel-agent"}, - }) - latest, found, err = updater.DetectLatest("henrygd/beszel") - - if err != nil { - fmt.Println("Error checking for updates:", err) - os.Exit(1) - } - - if !found { - fmt.Println("No updates found") - os.Exit(0) - } - - fmt.Println("Latest version:", latest.Version) - - if latest.Version.LTE(currentVersion) { - fmt.Println("You are up to date") - return - } - - var binaryPath string - fmt.Printf("Updating from %s to %s...\n", currentVersion, latest.Version) - binaryPath, err = os.Executable() - if err != nil { - fmt.Println("Error getting binary path:", err) - os.Exit(1) - } - err = selfupdate.UpdateTo(latest.AssetURL, binaryPath) - if err != nil { - fmt.Println("Please try rerunning with sudo. Error:", err) - os.Exit(1) - } - fmt.Printf("Successfully updated to %s\n\n%s\n", latest.Version, strings.TrimSpace(latest.ReleaseNotes)) -} diff --git a/cmd/agent/agent.go b/cmd/agent/agent.go index 06ab7d0..2918644 100644 --- a/cmd/agent/agent.go +++ b/cmd/agent/agent.go @@ -1 +1,48 @@ package main + +import ( + "beszel" + "beszel/internal/agent" + "beszel/internal/update" + "fmt" + "log" + "os" + "strings" +) + +func main() { + // handle flags / subcommands + if len(os.Args) > 1 { + switch os.Args[1] { + case "-v": + fmt.Println(beszel.AppName+"-agent", beszel.Version) + case "update": + update.UpdateBeszelAgent() + } + os.Exit(0) + } + + var pubKey []byte + if pubKeyEnv, exists := os.LookupEnv("KEY"); exists { + pubKey = []byte(pubKeyEnv) + } else { + log.Fatal("KEY environment variable is not set") + } + + var port string + + if p, exists := os.LookupEnv("PORT"); exists { + // allow passing an address in the form of "127.0.0.1:45876" + if !strings.Contains(port, ":") { + port = ":" + port + } + port = p + } else { + port = ":45876" + } + + a := agent.NewAgent(pubKey, port) + + a.Run() + +} diff --git a/agent/main.go b/internal/agent/agent.go similarity index 86% rename from agent/main.go rename to internal/agent/agent.go index 9134dae..5b44e53 100644 --- a/agent/main.go +++ b/internal/agent/agent.go @@ -1,6 +1,8 @@ -package main +package agent import ( + "beszel/internal/entities/container" + "beszel/internal/entities/system" "context" "encoding/json" "fmt" @@ -15,38 +17,49 @@ import ( "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" + + sshServer "github.com/gliderlabs/ssh" psutilNet "github.com/shirou/gopsutil/v4/net" ) var Version = "0.1.2" -var containerStatsMap = make(map[string]*PrevContainerStats) +var containerStatsMap = make(map[string]*container.PrevContainerStats) var containerStatsMutex = &sync.Mutex{} -var sem = make(chan struct{}, 15) - -func acquireSemaphore() { - sem <- struct{}{} +type Agent struct { + port string + pubKey []byte + sem chan struct{} } -func releaseSemaphore() { - <-sem +func NewAgent(pubKey []byte, port string) *Agent { + return &Agent{ + pubKey: pubKey, + sem: make(chan struct{}, 15), + } } -var diskIoStats = DiskIoStats{ +func (a *Agent) acquireSemaphore() { + a.sem <- struct{}{} +} + +func (a *Agent) releaseSemaphore() { + <-a.sem +} + +var diskIoStats = system.DiskIoStats{ Read: 0, Write: 0, Time: time.Now(), Filesystem: "", } -var netIoStats = NetIoStats{ +var netIoStats = system.NetIoStats{ BytesRecv: 0, BytesSent: 0, Time: time.Now(), @@ -56,8 +69,8 @@ var netIoStats = NetIoStats{ // client for docker engine api var dockerClient = newDockerClient() -func getSystemStats() (*SystemInfo, *SystemStats) { - systemStats := &SystemStats{} +func getSystemStats() (*system.SystemInfo, *system.SystemStats) { + systemStats := &system.SystemStats{} // cpu percent cpuPct, err := cpu.Percent(0, false) @@ -124,7 +137,7 @@ func getSystemStats() (*SystemInfo, *SystemStats) { netIoStats.Time = time.Now() } - systemInfo := &SystemInfo{ + systemInfo := &system.SystemInfo{ Cpu: systemStats.Cpu, MemPct: systemStats.MemPct, DiskPct: systemStats.DiskPct, @@ -150,21 +163,21 @@ func getSystemStats() (*SystemInfo, *SystemStats) { } -func getDockerStats() ([]*ContainerStats, error) { +func (a *Agent) getDockerStats() ([]*container.ContainerStats, error) { resp, err := dockerClient.Get("http://localhost/containers/json") if err != nil { closeIdleConnections(err) - return []*ContainerStats{}, err + return []*container.ContainerStats{}, err } defer resp.Body.Close() - var containers []*Container + var containers []*container.Container if err := json.NewDecoder(resp.Body).Decode(&containers); err != nil { log.Printf("Error decoding containers: %+v\n", err) - return []*ContainerStats{}, err + return []*container.ContainerStats{}, err } - containerStats := make([]*ContainerStats, 0, len(containers)) + containerStats := make([]*container.ContainerStats, 0, len(containers)) // store valid ids to clean up old container ids from map validIds := make(map[string]struct{}, len(containers)) @@ -183,7 +196,7 @@ func getDockerStats() ([]*ContainerStats, error) { wg.Add(1) go func() { defer wg.Done() - cstats, err := getContainerStats(ctr) + cstats, err := a.getContainerStats(ctr) if err != nil { // Check if the error is a network timeout if netErr, ok := err.(net.Error); ok && netErr.Timeout() { @@ -194,7 +207,7 @@ func getDockerStats() ([]*ContainerStats, error) { deleteContainerStatsSync(ctr.IdShort) } // retry once - cstats, err = getContainerStats(ctr) + cstats, err = a.getContainerStats(ctr) if err != nil { log.Printf("Error getting container stats: %+v\n", err) return @@ -216,17 +229,17 @@ func getDockerStats() ([]*ContainerStats, error) { return containerStats, nil } -func getContainerStats(ctr *Container) (*ContainerStats, error) { +func (a *Agent) getContainerStats(ctr *container.Container) (*container.ContainerStats, error) { // use semaphore to limit concurrency - acquireSemaphore() - defer releaseSemaphore() + a.acquireSemaphore() + defer a.releaseSemaphore() resp, err := dockerClient.Get("http://localhost/containers/" + ctr.IdShort + "/stats?stream=0&one-shot=1") if err != nil { - return &ContainerStats{}, err + return &container.ContainerStats{}, err } defer resp.Body.Close() - var statsJson CStats + var statsJson system.CStats if err := json.NewDecoder(resp.Body).Decode(&statsJson); err != nil { panic(err) } @@ -246,7 +259,7 @@ func getContainerStats(ctr *Container) (*ContainerStats, error) { // add empty values if they doesn't exist in map stats, initialized := containerStatsMap[ctr.IdShort] if !initialized { - stats = &PrevContainerStats{} + stats = &container.PrevContainerStats{} containerStatsMap[ctr.IdShort] = stats } @@ -255,7 +268,7 @@ func getContainerStats(ctr *Container) (*ContainerStats, error) { systemDelta := statsJson.CPUStats.SystemUsage - stats.Cpu[1] cpuPct := float64(cpuDelta) / float64(systemDelta) * 100 if cpuPct > 100 { - return &ContainerStats{}, fmt.Errorf("%s cpu pct greater than 100: %+v", name, cpuPct) + return &container.ContainerStats{}, fmt.Errorf("%s cpu pct greater than 100: %+v", name, cpuPct) } stats.Cpu = [2]uint64{statsJson.CPUStats.CPUUsage.TotalUsage, statsJson.CPUStats.SystemUsage} @@ -277,7 +290,7 @@ func getContainerStats(ctr *Container) (*ContainerStats, error) { stats.Net.Recv = total_recv stats.Net.Time = time.Now() - cStats := &ContainerStats{ + cStats := &container.ContainerStats{ Name: name, Cpu: twoDecimals(cpuPct), Mem: bytesToMegabytes(float64(usedMemory)), @@ -294,14 +307,14 @@ func deleteContainerStatsSync(id string) { delete(containerStatsMap, id) } -func gatherStats() *SystemData { +func (a *Agent) gatherStats() *system.SystemData { systemInfo, systemStats := getSystemStats() - stats := &SystemData{ + stats := &system.SystemData{ Stats: systemStats, Info: systemInfo, - Containers: []*ContainerStats{}, + Containers: []*container.ContainerStats{}, } - containerStats, err := getDockerStats() + containerStats, err := a.getDockerStats() if err == nil { stats.Containers = containerStats } @@ -309,9 +322,9 @@ func gatherStats() *SystemData { return stats } -func startServer(addr string, pubKey []byte) { +func (a *Agent) startServer(addr string, pubKey []byte) { sshServer.Handle(func(s sshServer.Session) { - stats := gatherStats() + stats := a.gatherStats() var jsonStats []byte jsonStats, _ = json.Marshal(stats) io.WriteString(s, string(jsonStats)) @@ -330,24 +343,7 @@ func startServer(addr string, pubKey []byte) { } } -func main() { - // handle flags / subcommands - if len(os.Args) > 1 { - switch os.Args[1] { - case "-v": - fmt.Println("beszel-agent", Version) - case "update": - updateBeszel() - } - os.Exit(0) - } - - var pubKey []byte - if pubKeyEnv, exists := os.LookupEnv("KEY"); exists { - pubKey = []byte(pubKeyEnv) - } else { - log.Fatal("KEY environment variable is not set") - } +func (a *Agent) Run() { if filesystem, exists := os.LookupEnv("FILESYSTEM"); exists { diskIoStats.Filesystem = filesystem @@ -358,15 +354,7 @@ func main() { initializeDiskIoStats() initializeNetIoStats() - if port, exists := os.LookupEnv("PORT"); exists { - // allow passing an address in the form of "127.0.0.1:45876" - if !strings.Contains(port, ":") { - port = ":" + port - } - startServer(port, pubKey) - } else { - startServer(":45876", pubKey) - } + a.startServer(a.port, a.pubKey) } func bytesToMegabytes(b float64) float64 { diff --git a/internal/entities/container/stats.go b/internal/entities/container/stats.go index e99a9f4..80aab5e 100644 --- a/internal/entities/container/stats.go +++ b/internal/entities/container/stats.go @@ -1,5 +1,29 @@ package container +import "time" + +type Container struct { + Id string + IdShort string + Names []string + Status 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 + // HostConfig struct { + // NetworkMode string `json:",omitempty"` + // Annotations map[string]string `json:",omitempty"` + // } + // NetworkSettings *SummaryNetworkSettings + // Mounts []MountPoint +} + type ContainerStats struct { Name string `json:"n"` Cpu float64 `json:"c"` @@ -7,3 +31,12 @@ type ContainerStats struct { NetworkSent float64 `json:"ns"` NetworkRecv float64 `json:"nr"` } + +type PrevContainerStats struct { + Cpu [2]uint64 + Net struct { + Sent uint64 + Recv uint64 + Time time.Time + } +} diff --git a/internal/entities/system/stats.go b/internal/entities/system/stats.go index 43f591c..caafcc8 100644 --- a/internal/entities/system/stats.go +++ b/internal/entities/system/stats.go @@ -1,5 +1,7 @@ package system +import "time" + type SystemStats struct { Cpu float64 `json:"cpu"` Mem float64 `json:"m"` @@ -16,3 +18,104 @@ type SystemStats struct { NetworkSent float64 `json:"ns"` NetworkRecv float64 `json:"nr"` } + +type DiskIoStats struct { + Read uint64 + Write uint64 + Time time.Time + Filesystem string +} + +type NetIoStats struct { + BytesRecv uint64 + BytesSent uint64 + Time time.Time + Name string +} + +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 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"` + // Networks request version >=1.21 + Networks map[string]NetworkStats + + // Shared stats + CPUStats CPUStats `json:"cpu_stats,omitempty"` + // PreCPUStats CPUStats `json:"precpu_stats,omitempty"` // "Pre"="Previous" + MemoryStats MemoryStats `json:"memory_stats,omitempty"` +} + +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 NetworkStats struct { + // Bytes received. Windows and Linux. + RxBytes uint64 `json:"rx_bytes"` + // Bytes sent. Windows and Linux. + TxBytes uint64 `json:"tx_bytes"` +} diff --git a/internal/entities/system/system.go b/internal/entities/system/system.go index 39a60b7..941ce7f 100644 --- a/internal/entities/system/system.go +++ b/internal/entities/system/system.go @@ -14,7 +14,7 @@ type SystemInfo struct { } type SystemData struct { - Stats SystemStats `json:"stats"` - Info SystemInfo `json:"info"` - Containers []container.ContainerStats `json:"container"` + Stats *SystemStats `json:"stats"` + Info *SystemInfo `json:"info"` + Containers []*container.ContainerStats `json:"container"` } diff --git a/internal/update/update.go b/internal/update/update.go index e4721d1..9ad5c82 100644 --- a/internal/update/update.go +++ b/internal/update/update.go @@ -54,3 +54,47 @@ func UpdateBeszel(cmd *cobra.Command, args []string) { } fmt.Printf("Successfully updated to %s\n\n%s\n", latest.Version, strings.TrimSpace(latest.ReleaseNotes)) } + +func UpdateBeszelAgent() { + var latest *selfupdate.Release + var found bool + var err error + currentVersion := semver.MustParse(beszel.Version) + fmt.Println("beszel-agent", currentVersion) + fmt.Println("Checking for updates...") + updater, _ := selfupdate.NewUpdater(selfupdate.Config{ + Filters: []string{"beszel-agent"}, + }) + latest, found, err = updater.DetectLatest("henrygd/beszel") + + if err != nil { + fmt.Println("Error checking for updates:", err) + os.Exit(1) + } + + if !found { + fmt.Println("No updates found") + os.Exit(0) + } + + fmt.Println("Latest version:", latest.Version) + + if latest.Version.LTE(currentVersion) { + fmt.Println("You are up to date") + return + } + + var binaryPath string + fmt.Printf("Updating from %s to %s...\n", currentVersion, latest.Version) + binaryPath, err = os.Executable() + if err != nil { + fmt.Println("Error getting binary path:", err) + os.Exit(1) + } + err = selfupdate.UpdateTo(latest.AssetURL, binaryPath) + if err != nil { + fmt.Println("Please try rerunning with sudo. Error:", err) + os.Exit(1) + } + fmt.Printf("Successfully updated to %s\n\n%s\n", latest.Version, strings.TrimSpace(latest.ReleaseNotes)) +}