main.go
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172
package main

import (	"encoding/json"
	"fmt"
	"log"
	"os"
	"os/signal"
	"syscall"
	"time"

	"github.com/shirou/gopsutil/v3/cpu"
	"github.com/shirou/gopsutil/v3/disk"
	"github.com/shirou/gopsutil/v3/mem"
	"github.com/shirou/gopsutil/v3/net")

// SystemStats holds aggregated system resource statistics
type SystemStats struct {
	Timestamp   time.Time `json:"timestamp"`
	CPUUsage    float64   `json:"cpu_usage_percent"` // Overall CPU usage (%)
	MemoryStats struct {
		Total     uint64  `json:"total_bytes"`
		Used      uint64  `json:"used_bytes"`
		UsedPercent float64 `json:"used_percent"`
	} `json:"memory"`
	DiskStats   []struct {
		MountPoint string  `json:"mount_point"`
		Total      uint64  `json:"total_bytes"`
		Used       uint64  `json:"used_bytes"`
		UsedPercent float64 `json:"used_percent"`
	} `json:"disk"`
	NetworkStats []struct {
		Interface  string  `json:"interface"`
		BytesSent  uint64  `json:"bytes_sent"`
		BytesRecv  uint64  `json:"bytes_recv"`
	} `json:"network"`
}

// SystemMonitor collects system stats at specified intervals
type SystemMonitor struct {
	interval time.Duration // Sampling interval
	stopChan chan struct{}
}

// NewSystemMonitor creates a new monitor with sampling interval
func NewSystemMonitor(interval time.Duration) *SystemMonitor {
	return &SystemMonitor{
		interval: interval,
		stopChan: make(chan struct{}),
	}
}

// CollectStats gathers a single snapshot of system statistics
func (m *SystemMonitor) CollectStats() (*SystemStats, error) {
	stats := &SystemStats{Timestamp: time.Now()}

	// 1. Collect CPU usage (percent over 1-second interval)
	cpuPercent, err := cpu.Percent(1*time.Second, false)
	if err != nil {
		return nil, fmt.Errorf("failed to get CPU usage: %v", err)
	}
	if len(cpuPercent) > 0 {
		stats.CPUUsage = cpuPercent[0]
	}

	// 2. Collect memory statistics
	memInfo, err := mem.VirtualMemory()
	if err != nil {
		return nil, fmt.Errorf("failed to get memory info: %v", err)
	}
	stats.MemoryStats.Total = memInfo.Total
	stats.MemoryStats.Used = memInfo.Used
	stats.MemoryStats.UsedPercent = memInfo.UsedPercent

	// 3. Collect disk statistics (all mounted partitions)
	diskParts, err := disk.Partitions(true)
	if err != nil {
		return nil, fmt.Errorf("failed to get disk partitions: %v", err)
	}
	for _, part := range diskParts {
		diskInfo, err := disk.Usage(part.Mountpoint)
		if err != nil {
			log.Printf("Failed to get disk usage for %s: %v", part.Mountpoint, err)
			continue
		}
		stats.DiskStats = append(stats.DiskStats, struct {
			MountPoint string  `json:"mount_point"`
			Total      uint64  `json:"total_bytes"`
			Used       uint64  `json:"used_bytes"`
			UsedPercent float64 `json:"used_percent"`
		}{
			MountPoint: part.Mountpoint,
			Total:      diskInfo.Total,
			Used:       diskInfo.Used,
			UsedPercent: diskInfo.UsedPercent,
		})
	}

	// 4. Collect network statistics (all interfaces)
	netIO, err := net.IOCounters(true)
	if err != nil {
		return nil, fmt.Errorf("failed to get network stats: %v", err)
	}
	for _, io := range netIO {
		stats.NetworkStats = append(stats.NetworkStats, struct {
			Interface  string  `json:"interface"`
			BytesSent  uint64  `json:"bytes_sent"`
			BytesRecv  uint64  `json:"bytes_recv"`
		}{
			Interface:  io.Name,
			BytesSent:  io.BytesSent,
			BytesRecv:  io.BytesRecv,
		})
	}

	return stats, nil
}

// Start begins periodic statistics collection and printing
func (m *SystemMonitor) Start() {
	log.Printf("System monitor started (sampling interval: %v)", m.interval)
	ticker := time.NewTicker(m.interval)
	defer ticker.Stop()

	for {
		select {
		case <-m.stopChan:
			log.Println("System monitor stopped")
			return
		case <-ticker.C:
			stats, err := m.CollectStats()
			if err != nil {
				log.Printf("Failed to collect stats: %v", err)
				continue
			}
			// Print stats as pretty-printed JSON
			jsonData, err := json.MarshalIndent(stats, "", "  ")
			if err != nil {
				log.Printf("Failed to marshal stats: %v", err)
				continue
			}
			fmt.Println("--- System Stats ---")
			fmt.Println(string(jsonData))
		}
	}
}

// Stop terminates the monitor
func (m *SystemMonitor) Stop() {
	close(m.stopChan)
}

// Example Usage
func main() {
	if len(os.Args) < 2 {
		log.Fatal("Usage: go run main.go <interval-seconds>")
	}
	intervalSecs := 0
	fmt.Sscanf(os.Args[1], "%d", &intervalSecs)
	if intervalSecs < 1 {
		intervalSecs = 5
	}

	// Create and start monitor
	monitor := NewSystemMonitor(time.Duration(intervalSecs) * time.Second)
	go monitor.Start()

	// Wait for SIGINT/SIGTERM
	sigChan := make(chan os.Signal, 1)
	signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
	<-sigChan
	monitor.Stop()
}

How It Works

Cross-platform sampler that periodically collects CPU, memory, disk, and network stats for health monitoring.

Uses system libraries to read metrics, aggregates them at intervals, prints JSON snapshots, and supports graceful stop via signals so the sampler exits cleanly.

Key Concepts

  • 1Periodic ticker drives metric collection without busy loops.
  • 2Structures capture CPU load, memory usage, disk capacity, and network counters.
  • 3Signal handling stops sampling cleanly for long-running agents.

When to Use This Pattern

  • Building lightweight node exporters or sidecar monitors.
  • Collecting host metrics for dashboards and alerts.
  • Baseline performance measurements during testing.

Best Practices

  • Choose a sampling interval that balances fidelity and overhead.
  • Run with permissions sufficient for reading system stats only.
  • Emit metrics in a structured format for parsers such as JSON or Prometheus.
Go Version1.17
Difficultyintermediate
Production ReadyYes
Lines of Code172