|
|
|
|
|
|
|
|
| package agent
|
|
|
| import (
|
| "log/slog"
|
| "strings"
|
| "sync"
|
| "time"
|
|
|
| "github.com/gliderlabs/ssh"
|
| "github.com/henrygd/beszel"
|
| "github.com/henrygd/beszel/agent/deltatracker"
|
| "github.com/henrygd/beszel/agent/utils"
|
| "github.com/henrygd/beszel/internal/common"
|
| "github.com/henrygd/beszel/internal/entities/system"
|
| gossh "golang.org/x/crypto/ssh"
|
| )
|
|
|
| const defaultDataCacheTimeMs uint16 = 60_000
|
|
|
| type Agent struct {
|
| sync.Mutex
|
| debug bool
|
| zfs bool
|
| memCalc string
|
| fsNames []string
|
| fsStats map[string]*system.FsStats
|
| diskPrev map[uint16]map[string]prevDisk
|
| diskUsageCacheDuration time.Duration
|
| lastDiskUsageUpdate time.Time
|
| netInterfaces map[string]struct{}
|
| netIoStats map[uint16]system.NetIoStats
|
| netInterfaceDeltaTrackers map[uint16]*deltatracker.DeltaTracker[string, uint64]
|
| dockerManager *dockerManager
|
| sensorConfig *SensorConfig
|
| systemInfo system.Info
|
| systemDetails system.Details
|
| detailsDirty bool
|
| gpuManager *GPUManager
|
| cache *systemDataCache
|
| connectionManager *ConnectionManager
|
| handlerRegistry *HandlerRegistry
|
| server *ssh.Server
|
| dataDir string
|
| keys []gossh.PublicKey
|
| smartManager *SmartManager
|
| systemdManager *systemdManager
|
| }
|
|
|
|
|
|
|
| func NewAgent(dataDir ...string) (agent *Agent, err error) {
|
| agent = &Agent{
|
| fsStats: make(map[string]*system.FsStats),
|
| cache: NewSystemDataCache(),
|
| }
|
|
|
|
|
| agent.diskPrev = make(map[uint16]map[string]prevDisk)
|
|
|
| agent.netIoStats = make(map[uint16]system.NetIoStats)
|
| agent.netInterfaceDeltaTrackers = make(map[uint16]*deltatracker.DeltaTracker[string, uint64])
|
|
|
| agent.dataDir, err = GetDataDir(dataDir...)
|
| if err != nil {
|
| slog.Warn("Data directory not found")
|
| } else {
|
| slog.Info("Data directory", "path", agent.dataDir)
|
| }
|
|
|
| agent.memCalc, _ = utils.GetEnv("MEM_CALC")
|
| agent.sensorConfig = agent.newSensorConfig()
|
|
|
|
|
| if diskUsageCache, exists := utils.GetEnv("DISK_USAGE_CACHE"); exists {
|
| if duration, err := time.ParseDuration(diskUsageCache); err == nil {
|
| agent.diskUsageCacheDuration = duration
|
| slog.Info("DISK_USAGE_CACHE", "duration", duration)
|
| } else {
|
| slog.Warn("Invalid DISK_USAGE_CACHE", "err", err)
|
| }
|
| }
|
|
|
|
|
| if logLevelStr, exists := utils.GetEnv("LOG_LEVEL"); exists {
|
| switch strings.ToLower(logLevelStr) {
|
| case "debug":
|
| agent.debug = true
|
| slog.SetLogLoggerLevel(slog.LevelDebug)
|
| case "warn":
|
| slog.SetLogLoggerLevel(slog.LevelWarn)
|
| case "error":
|
| slog.SetLogLoggerLevel(slog.LevelError)
|
| }
|
| }
|
|
|
| slog.Debug(beszel.Version)
|
|
|
|
|
| agent.dockerManager = newDockerManager(agent)
|
|
|
|
|
| agent.refreshSystemDetails()
|
|
|
|
|
| if smartIntervalEnv, exists := utils.GetEnv("SMART_INTERVAL"); exists {
|
| if duration, err := time.ParseDuration(smartIntervalEnv); err == nil && duration > 0 {
|
| agent.systemDetails.SmartInterval = duration
|
| slog.Info("SMART_INTERVAL", "duration", duration)
|
| } else {
|
| slog.Warn("Invalid SMART_INTERVAL", "err", err)
|
| }
|
| }
|
|
|
|
|
| agent.connectionManager = newConnectionManager(agent)
|
|
|
|
|
| agent.handlerRegistry = NewHandlerRegistry()
|
|
|
|
|
| agent.initializeDiskInfo()
|
|
|
|
|
| agent.initializeNetIoStats()
|
|
|
| agent.systemdManager, err = newSystemdManager()
|
| if err != nil {
|
| slog.Debug("Systemd", "err", err)
|
| }
|
|
|
| agent.smartManager, err = NewSmartManager()
|
| if err != nil {
|
| slog.Debug("SMART", "err", err)
|
| }
|
|
|
|
|
| agent.gpuManager, err = NewGPUManager()
|
| if err != nil {
|
| slog.Debug("GPU", "err", err)
|
| }
|
|
|
|
|
| if agent.debug {
|
| slog.Debug("Stats", "data", agent.gatherStats(common.DataRequestOptions{CacheTimeMs: defaultDataCacheTimeMs, IncludeDetails: true}))
|
| }
|
|
|
| return agent, nil
|
| }
|
|
|
| func (a *Agent) gatherStats(options common.DataRequestOptions) *system.CombinedData {
|
| a.Lock()
|
| defer a.Unlock()
|
|
|
| cacheTimeMs := options.CacheTimeMs
|
| data, isCached := a.cache.Get(cacheTimeMs)
|
| if isCached {
|
| slog.Debug("Cached data", "cacheTimeMs", cacheTimeMs)
|
| return data
|
| }
|
|
|
| *data = system.CombinedData{
|
| Stats: a.getSystemStats(cacheTimeMs),
|
| Info: a.systemInfo,
|
| }
|
|
|
|
|
|
|
| if a.dockerManager != nil {
|
| if containerStats, err := a.dockerManager.getDockerStats(cacheTimeMs); err == nil {
|
| data.Containers = containerStats
|
| slog.Debug("Containers", "data", data.Containers)
|
| } else {
|
| slog.Debug("Containers", "err", err)
|
| }
|
| }
|
|
|
|
|
| if a.systemdManager != nil && cacheTimeMs == defaultDataCacheTimeMs {
|
| totalCount := uint16(a.systemdManager.getServiceStatsCount())
|
| if totalCount > 0 {
|
| numFailed := a.systemdManager.getFailedServiceCount()
|
| data.Info.Services = []uint16{totalCount, numFailed}
|
| }
|
| if a.systemdManager.hasFreshStats {
|
| data.SystemdServices = a.systemdManager.getServiceStats(nil, false)
|
| }
|
| }
|
|
|
| data.Stats.ExtraFs = make(map[string]*system.FsStats)
|
| data.Info.ExtraFsPct = make(map[string]float64)
|
| for name, stats := range a.fsStats {
|
| if !stats.Root && stats.DiskTotal > 0 {
|
|
|
| key := name
|
| if stats.Name != "" {
|
| key = stats.Name
|
| }
|
| data.Stats.ExtraFs[key] = stats
|
|
|
| if stats.DiskTotal > 0 {
|
| pct := utils.TwoDecimals((stats.DiskUsed / stats.DiskTotal) * 100)
|
| data.Info.ExtraFsPct[key] = pct
|
| }
|
| }
|
| }
|
| slog.Debug("Extra FS", "data", data.Stats.ExtraFs)
|
|
|
| a.cache.Set(data, cacheTimeMs)
|
|
|
| return a.attachSystemDetails(data, cacheTimeMs, options.IncludeDetails)
|
| }
|
|
|
|
|
| func (a *Agent) Start(serverOptions ServerOptions) error {
|
| a.keys = serverOptions.Keys
|
| return a.connectionManager.Start(serverOptions)
|
| }
|
|
|
| func (a *Agent) getFingerprint() string {
|
| return GetFingerprint(a.dataDir, a.systemDetails.Hostname, a.systemDetails.CpuModel)
|
| }
|
|
|