diff --git a/.github/workflows/develop.yaml b/.github/workflows/develop.yaml index 5098cf7..7bc2f0f 100644 --- a/.github/workflows/develop.yaml +++ b/.github/workflows/develop.yaml @@ -16,6 +16,12 @@ jobs: - uses: hadolint/hadolint-action@v3.1.0 with: dockerfile: docker/Dockerfile + - name: Check gofmt + run: | + if [ -n "$(cd app && gofmt -l .)" ]; then + echo "gofmt: run 'gofmt -w app/'" + exit 1 + fi - uses: golangci/golangci-lint-action@v6 with: working-directory: app diff --git a/Makefile b/Makefile index b00f975..de6cd9a 100644 --- a/Makefile +++ b/Makefile @@ -29,6 +29,7 @@ install-tools: .PHONY: lint lint: + @if [ -n "$$(cd app && gofmt -l .)" ]; then echo "gofmt: run 'gofmt -w app/'"; exit 1; fi @cd app && golangci-lint run ./...; @helm lint helm/pi-agent; @hadolint docker/Dockerfile; diff --git a/app/collectors/cpu.go b/app/collectors/cpu.go index 27a0553..adc4a01 100644 --- a/app/collectors/cpu.go +++ b/app/collectors/cpu.go @@ -9,26 +9,26 @@ import ( "strings" ) -type Cpu struct{ +type Cpu struct { prevIdle uint64 - prevTotal uint64 - fileName string + prevTotal uint64 + fileName string } func NewCpu(path string) *Cpu { return &Cpu{fileName: path} } - -func (c *Cpu)Name() string { - return "cpu" + +func (c *Cpu) Name() string { + return "cpu" } -func (c *Cpu)Collect() (float64, error) { +func (c *Cpu) Collect() (float64, error) { // Open the file file, err := os.Open(c.fileName) if err != nil { log.Printf("Error opening file: %s", err) - return 0.0, err + return 0.0, err } // Ensure the file is closed the the function exits @@ -52,17 +52,17 @@ func (c *Cpu)Collect() (float64, error) { } // Calculate idleDelta - idleDelta := currentIdle - c.prevIdle + idleDelta := currentIdle - c.prevIdle // calculate currentTotal - var currentTotal uint64 - for _, stat := range stats { + var currentTotal uint64 + for _, stat := range stats { stat, err := strconv.ParseUint(stat, 10, 64) if err != nil { return 0.0, err } - currentTotal += uint64(stat) - } + currentTotal += uint64(stat) + } // values for first run if c.prevTotal == 0 { @@ -75,11 +75,11 @@ func (c *Cpu)Collect() (float64, error) { totalDelta := currentTotal - c.prevTotal // Usage Percent = (1 - idleDelta/totalDelta) * 100 - usagePercent := (1.0 - float64(idleDelta)/float64(totalDelta))* 100 + usagePercent := (1.0 - float64(idleDelta)/float64(totalDelta)) * 100 // Save current readings c.prevIdle = currentIdle c.prevTotal = currentTotal return usagePercent, nil -} \ No newline at end of file +} diff --git a/app/collectors/disk.go b/app/collectors/disk.go new file mode 100644 index 0000000..9cf0fb2 --- /dev/null +++ b/app/collectors/disk.go @@ -0,0 +1,29 @@ +package collectors + +import ( + "syscall" +) + +type Disk struct { + path string +} + +func NewDisk(path string) *Disk { + return &Disk{path: path} +} + +func (d *Disk) Name() string { + return "disk" +} + +func (d *Disk) Collect() (float64, error) { + // Call Statfs + var stat syscall.Statfs_t + err := syscall.Statfs(d.path, &stat) + if err != nil { + return 0.0, err + } + + usagePercent := (1.0 - float64(stat.Bavail)/float64(stat.Blocks)) * 100.0 + return usagePercent, nil +} diff --git a/app/collectors/disk_test.go b/app/collectors/disk_test.go new file mode 100644 index 0000000..7d632dd --- /dev/null +++ b/app/collectors/disk_test.go @@ -0,0 +1,29 @@ +package collectors + +import "testing" + +func TestDiskName(t *testing.T) { + d := &Disk{} + if d.Name() != "disk" { + t.Errorf("expected name 'disk', got %s", d.Name()) + } +} + +func TestDiskCollect_ReturnsValidPercent(t *testing.T) { + d := &Disk{path: "/tmp"} + val, err := d.Collect() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if val < 0 || val > 100 { + t.Errorf("expected value between 0 and 100, got %.2f", val) + } +} + +func TestDiskCollect_InvalidPath(t *testing.T) { + d := &Disk{path: "/nonexistent/path"} + _, err := d.Collect() + if err == nil { + t.Error("expected error for invalid path, got nil") + } +} diff --git a/app/collectors/memory.go b/app/collectors/memory.go index 60f0968..25bfcd0 100644 --- a/app/collectors/memory.go +++ b/app/collectors/memory.go @@ -9,24 +9,24 @@ import ( "strings" ) -type Memory struct{ +type Memory struct { fileName string } func NewMemory(path string) *Memory { return &Memory{fileName: path} } - -func (m *Memory)Name() string { - return "memory" + +func (m *Memory) Name() string { + return "memory" } -func (m *Memory)Collect() (float64, error) { +func (m *Memory) Collect() (float64, error) { // Open the file file, err := os.Open(m.fileName) if err != nil { log.Printf("Error opening file: %s", err) - return 0.0, err + return 0.0, err } // Ensure the file is closed the the function exits @@ -34,7 +34,7 @@ func (m *Memory)Collect() (float64, error) { // Create a new Scanner for the file scanner := bufio.NewScanner(file) - + // Get memTotal from line 0 if !scanner.Scan() { if err := scanner.Err(); err != nil { @@ -68,6 +68,6 @@ func (m *Memory)Collect() (float64, error) { } // Calculate usagePercent - usagePercent := float64(memTotal - memAvailable) / float64(memTotal) * 100 + usagePercent := float64(memTotal-memAvailable) / float64(memTotal) * 100 return usagePercent, nil -} \ No newline at end of file +} diff --git a/app/collectors/temperature.go b/app/collectors/temperature.go new file mode 100644 index 0000000..228fd13 --- /dev/null +++ b/app/collectors/temperature.go @@ -0,0 +1,38 @@ +package collectors + +import ( + "log" + "os" + "strconv" + "strings" +) + +type Temperature struct { + fileName string +} + +func NewTemperature(path string) *Temperature { + return &Temperature{fileName: path} +} + +func (t *Temperature) Name() string { + return "temp_f" +} + +func (t *Temperature) Collect() (float64, error) { + // Open the file + file, err := os.ReadFile(t.fileName) + if err != nil { + log.Printf("Error reading file: %s", err) + return 0.0, err + } + + tempC, err := strconv.ParseInt(strings.TrimSpace(string(file)), 10, 64) + if err != nil { + return 0.0, err + } + + // Calculate Fahrenheit + tempF := ((float64(tempC) / 1000.0) * (9.0 / 5.0)) + 32.0 + return tempF, nil +} diff --git a/app/collectors/temperature_test.go b/app/collectors/temperature_test.go new file mode 100644 index 0000000..61d4c66 --- /dev/null +++ b/app/collectors/temperature_test.go @@ -0,0 +1,84 @@ +package collectors + +import ( + "os" + "testing" +) + +func writeTempInfo(t *testing.T, content string) string { + t.Helper() + f, err := os.CreateTemp("", "sys-class-thermal-thermal_zone0-temp-*") + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { _ = os.Remove(f.Name()) }) + if _, err := f.WriteString(content); err != nil { + t.Fatal(err) + } + if err := f.Close(); err != nil { + t.Fatal(err) + } + return f.Name() +} + +func TestTempName(t *testing.T) { + tp := &Temperature{} + if tp.Name() != "temp_f" { + t.Errorf("expected name 'temp_f', got %s", tp.Name()) + } +} + +func TestTempCollect_ZeroVal(t *testing.T) { + // Celsius=0 → fahrenheight = 32.0 + content := "00000\n" + path := writeTempInfo(t, content) + tp := &Temperature{fileName: path} + + val, err := tp.Collect() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if val != 32.0 { + t.Errorf("expected 32.0, got %.2f", val) + } +} + +func TestTempCollect_PositiveVal(t *testing.T) { + // Celsius=50.634 → fahrenheight = 123.14 + content := "50634\n" + path := writeTempInfo(t, content) + tp := &Temperature{fileName: path} + + val, err := tp.Collect() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if val < 123.14 || val > 123.15 { + t.Errorf("expected ~123.14, got %.4f", val) + } +} + +func TestTempCollect_NegativeVal(t *testing.T) { + // Celsius=-6.666 → fahrenheight = 20.0 + content := "-6666\n" + path := writeTempInfo(t, content) + tp := &Temperature{fileName: path} + + val, err := tp.Collect() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if val < 20.0 || val > 20.01 { + t.Errorf("expected ~20.0, got %.4f", val) + } +} + +func TestTempCollect_EmptyFile(t *testing.T) { + path := writeTempInfo(t, "") + tp := &Temperature{fileName: path} + + _, err := tp.Collect() + if err == nil { + t.Error("expected error for empty file, got nil") + } +} diff --git a/app/main.go b/app/main.go index d52e1b7..8519900 100644 --- a/app/main.go +++ b/app/main.go @@ -9,16 +9,18 @@ import ( ) func main() { - var seconds int + var schedule int - flag.IntVar(&seconds, "seconds", 300, "the time between metric collection cycles") + flag.IntVar(&schedule, "schedule", 300, "seconds between metric collection cycles") flag.Parse() - log.Printf("Seconds: %d\n", seconds) + log.Printf("Schedule: %d seconds\n", schedule) - metrics := []collectors.Collector{collectors.NewCpu("/host/proc/stat"), collectors.NewMemory("/host/proc/meminfo")} + metrics := []collectors.Collector{collectors.NewCpu("/host/proc/stat"), collectors.NewMemory("/host/proc/meminfo"), + collectors.NewTemperature("/host/sys/class/thermal/thermal_zone0/temp"), + collectors.NewDisk("/host/root/")} - ticker := time.NewTicker(time.Duration(seconds) * time.Second) + ticker := time.NewTicker(time.Duration(schedule) * time.Second) for range ticker.C { for _, c := range metrics { val, err := c.Collect() @@ -26,7 +28,7 @@ func main() { log.Printf("Couldn't get metric value: %s\n", err) continue } - log.Printf("Found metric value: %.2f\n", val) + log.Printf("%s: %.2f\n", c.Name(), val) } } } diff --git a/helm/pi-agent/templates/daemonset.yaml b/helm/pi-agent/templates/daemonset.yaml index 2eeabf1..7fe193a 100644 --- a/helm/pi-agent/templates/daemonset.yaml +++ b/helm/pi-agent/templates/daemonset.yaml @@ -18,10 +18,19 @@ spec: - name: {{ .Chart.Name }} image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" imagePullPolicy: {{ .Values.image.pullPolicy }} + args: + - "--schedule" + - "{{ .Values.collector.schedule }}" volumeMounts: - name: sysfs mountPath: /host/sys readOnly: true + - name: procfs + mountPath: /host/proc + readOnly: true + - name: rootfs + mountPath: /host/root + readOnly: true tolerations: # Run on all nodes including control plane - operator: Exists @@ -29,3 +38,9 @@ spec: - name: sysfs hostPath: path: /sys + - name: procfs + hostPath: + path: /proc + - name: rootfs + hostPath: + path: /