// Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: BUSL-1.1 package hoststats import ( "context" "fmt" "math" "runtime" "sync" "time" "github.com/armon/go-metrics" "github.com/hashicorp/go-hclog" "github.com/shirou/gopsutil/v3/disk" "github.com/shirou/gopsutil/v3/host" "github.com/shirou/gopsutil/v3/mem" ) // Collector collects host resource usage stats type Collector struct { numCores int cpuCalculator map[string]*cpuStatsCalculator hostStats *HostStats hostStatsLock sync.RWMutex dataDir string metrics Metrics baseLabels []metrics.Label logger hclog.Logger } // NewCollector returns a Collector. The dataDir is passed in // so that we can present the disk related statistics for the mountpoint where the dataDir exists func NewCollector(ctx context.Context, logger hclog.Logger, dataDir string, opts ...CollectorOption) *Collector { logger = logger.Named("host_stats") collector := initCollector(logger, dataDir) go collector.loop(ctx) return collector } // initCollector initializes the Collector but does not start the collection loop func initCollector(logger hclog.Logger, dataDir string, opts ...CollectorOption) *Collector { numCores := runtime.NumCPU() statsCalculator := make(map[string]*cpuStatsCalculator) collector := &Collector{ cpuCalculator: statsCalculator, numCores: numCores, logger: logger, dataDir: dataDir, } for _, opt := range opts { opt(collector) } if collector.metrics == nil { collector.metrics = metrics.Default() } return collector } func (c *Collector) loop(ctx context.Context) { // Start collecting host stats right away and then keep collecting every // collection interval next := time.NewTimer(0) defer next.Stop() for { select { case <-next.C: c.collect() next.Reset(hostStatsCollectionInterval) c.Stats().Emit(c.metrics, c.baseLabels) case <-ctx.Done(): return } } } // collect will collect stats related to resource usage of the host func (c *Collector) collect() { hs := &HostStats{Timestamp: time.Now().UTC().UnixNano()} // Determine up-time uptime, err := host.Uptime() if err != nil { c.logger.Debug("failed to collect uptime stats", "error", err) uptime = 0 } hs.Uptime = uptime // Collect memory stats mstats, err := c.collectMemoryStats() if err != nil { c.logger.Debug("failed to collect memory stats", "error", err) mstats = &MemoryStats{} } hs.Memory = mstats // Collect cpu stats cpus, err := c.collectCPUStats() if err != nil { c.logger.Debug("failed to collect cpu stats", "error", err) cpus = []*CPUStats{} } hs.CPU = cpus // Collect disk stats if c.dataDir != "" { diskStats, err := c.collectDiskStats(c.dataDir) if err != nil { c.logger.Debug("failed to collect dataDir disk stats", "error", err) } hs.DataDirStats = diskStats } // Update the collected status object. c.hostStatsLock.Lock() c.hostStats = hs c.hostStatsLock.Unlock() } func (c *Collector) collectDiskStats(dir string) (*DiskStats, error) { usage, err := disk.Usage(dir) if err != nil { return nil, fmt.Errorf("failed to collect disk usage stats: %w", err) } return c.toDiskStats(usage), nil } func (c *Collector) collectMemoryStats() (*MemoryStats, error) { memStats, err := mem.VirtualMemory() if err != nil { return nil, err } mem := &MemoryStats{ Total: memStats.Total, Available: memStats.Available, Used: memStats.Used, UsedPercent: memStats.UsedPercent, Free: memStats.Free, } return mem, nil } // Stats returns the host stats that has been collected func (c *Collector) Stats() *HostStats { c.hostStatsLock.RLock() defer c.hostStatsLock.RUnlock() if c.hostStats == nil { return &HostStats{} } return c.hostStats.Clone() } // toDiskStats merges UsageStat and PartitionStat to create a DiskStat func (c *Collector) toDiskStats(usage *disk.UsageStat) *DiskStats { ds := DiskStats{ Size: usage.Total, Used: usage.Used, Available: usage.Free, UsedPercent: usage.UsedPercent, InodesUsedPercent: usage.InodesUsedPercent, Path: usage.Path, } if math.IsNaN(ds.UsedPercent) { ds.UsedPercent = 0.0 } if math.IsNaN(ds.InodesUsedPercent) { ds.InodesUsedPercent = 0.0 } return &ds } type CollectorOption func(c *Collector) func WithMetrics(m *metrics.Metrics) CollectorOption { return func(c *Collector) { c.metrics = m } } func WithBaseLabels(labels []metrics.Label) CollectorOption { return func(c *Collector) { c.baseLabels = labels } }