-
-
Notifications
You must be signed in to change notification settings - Fork 12
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(linux): ✨ add Linux device IO rate sensors
- Loading branch information
Showing
8 changed files
with
338 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -17,7 +17,8 @@ | |
"dbusx", | ||
"device", | ||
"codecov", | ||
"translations" | ||
"translations", | ||
"logging" | ||
], | ||
"go.testFlags": ["-v"] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,192 @@ | ||
// Copyright (c) 2024 Joshua Rich <[email protected]> | ||
// | ||
// This software is released under the MIT License. | ||
// https://opensource.org/licenses/MIT | ||
|
||
package disk | ||
|
||
import ( | ||
"context" | ||
"time" | ||
|
||
"github.com/iancoleman/strcase" | ||
"github.com/rs/zerolog/log" | ||
|
||
"github.com/joshuar/go-hass-agent/internal/device/helpers" | ||
"github.com/joshuar/go-hass-agent/internal/hass/sensor" | ||
"github.com/joshuar/go-hass-agent/internal/hass/sensor/types" | ||
"github.com/joshuar/go-hass-agent/internal/linux" | ||
diskstats "github.com/joshuar/go-hass-agent/pkg/linux/proc" | ||
) | ||
|
||
type diskIOSensor struct { | ||
stats map[diskstats.DiskStat]uint64 | ||
device string | ||
linux.Sensor | ||
prev uint64 | ||
} | ||
|
||
type diskIOSensorAttributes struct { | ||
DataSource string `json:"Data Source"` | ||
NativeUnit string `json:"native_unit_of_measurement,omitempty"` | ||
Sectors uint64 `json:"Total Sectors,omitempty"` | ||
Time uint64 `json:"Total Milliseconds,omitempty"` | ||
} | ||
|
||
type sensors struct { | ||
totalReads *diskIOSensor | ||
totalWrites *diskIOSensor | ||
readRate *diskIOSensor | ||
writeRate *diskIOSensor | ||
} | ||
|
||
func (s *diskIOSensor) Name() string { | ||
return s.device + " " + s.SensorTypeValue.String() | ||
} | ||
|
||
func (s *diskIOSensor) ID() string { | ||
return s.device + "_" + strcase.ToSnake(s.SensorTypeValue.String()) | ||
} | ||
|
||
func (s *diskIOSensor) Attributes() any { | ||
switch s.SensorTypeValue { | ||
case linux.SensorDiskReads: | ||
return &diskIOSensorAttributes{ | ||
DataSource: linux.DataSrcProcfs, | ||
Sectors: s.stats[diskstats.TotalSectorsRead], | ||
Time: s.stats[diskstats.TotalTimeReading], | ||
} | ||
case linux.SensorDiskWrites: | ||
return &diskIOSensorAttributes{ | ||
DataSource: linux.DataSrcProcfs, | ||
Sectors: s.stats[diskstats.TotalSectorsWritten], | ||
Time: s.stats[diskstats.TotalTimeWriting], | ||
} | ||
case linux.SensorDiskReadRate: | ||
return &diskIOSensorAttributes{ | ||
DataSource: linux.DataSrcProcfs, | ||
NativeUnit: "KB/s", | ||
} | ||
case linux.SensorDiskWriteRate: | ||
return &diskIOSensorAttributes{ | ||
DataSource: linux.DataSrcProcfs, | ||
NativeUnit: "KB/s", | ||
} | ||
} | ||
return nil | ||
} | ||
|
||
func (s *diskIOSensor) Icon() string { | ||
switch s.SensorTypeValue { | ||
case linux.SensorDiskReads: | ||
return "mdi:file-upload" | ||
case linux.SensorDiskWrites: | ||
return "mdi:file-download" | ||
} | ||
return "mdi:file" | ||
} | ||
|
||
func (s *diskIOSensor) update(stats map[diskstats.DiskStat]uint64, delta time.Duration) { | ||
s.stats = stats | ||
var curr uint64 | ||
switch s.SensorTypeValue { | ||
case linux.SensorDiskReads: | ||
s.Value = s.stats[diskstats.TotalReads] | ||
case linux.SensorDiskWrites: | ||
s.Value = s.stats[diskstats.TotalWrites] | ||
case linux.SensorDiskReadRate: | ||
curr = s.stats[diskstats.TotalSectorsRead] | ||
case linux.SensorDiskWriteRate: | ||
curr = s.stats[diskstats.TotalSectorsWritten] | ||
} | ||
if s.SensorTypeValue == linux.SensorDiskReadRate || s.SensorTypeValue == linux.SensorDiskWriteRate { | ||
if uint64(delta.Seconds()) > 0 { | ||
log.Trace().Msgf("%s IO rate calc: (%d - %d) / uint64(%d) / 2", s.device, curr, s.prev, uint64(delta.Seconds())) | ||
s.Value = (curr - s.prev) / uint64(delta.Seconds()) / 2 | ||
} | ||
s.prev = curr | ||
} | ||
} | ||
|
||
func newDiskIOSensor(device string, sensorType linux.SensorTypeValue) *diskIOSensor { | ||
s := &diskIOSensor{ | ||
device: device, | ||
Sensor: linux.Sensor{ | ||
DeviceClassValue: types.DeviceClassDataSize, | ||
StateClassValue: types.StateClassTotalIncreasing, | ||
SensorTypeValue: sensorType, | ||
IsDiagnostic: true, | ||
}, | ||
} | ||
return s | ||
} | ||
|
||
func newDiskIORateSensor(device string, sensorType linux.SensorTypeValue) *diskIOSensor { | ||
s := &diskIOSensor{ | ||
device: device, | ||
Sensor: linux.Sensor{ | ||
DeviceClassValue: types.DeviceClassDataRate, | ||
StateClassValue: types.StateClassMeasurement, | ||
UnitsString: "KB/s", | ||
SensorTypeValue: sensorType, | ||
}, | ||
} | ||
return s | ||
} | ||
|
||
func newDevice(dev string) *sensors { | ||
return &sensors{ | ||
totalReads: newDiskIOSensor(dev, linux.SensorDiskReads), | ||
totalWrites: newDiskIOSensor(dev, linux.SensorDiskWrites), | ||
readRate: newDiskIORateSensor(dev, linux.SensorDiskReadRate), | ||
writeRate: newDiskIORateSensor(dev, linux.SensorDiskWriteRate), | ||
} | ||
} | ||
|
||
func IOUpdater(ctx context.Context) chan sensor.Details { | ||
sensorCh := make(chan sensor.Details) | ||
newStats, err := diskstats.ReadDiskstats() | ||
if err != nil { | ||
log.Warn().Err(err).Msg("Error reading disk stats from procfs. Will not send disk rate sensors.") | ||
close(sensorCh) | ||
return sensorCh | ||
} | ||
devices := make(map[string]*sensors) | ||
for dev := range newStats { | ||
devices[dev] = newDevice(dev) | ||
} | ||
diskIOstats := func(delta time.Duration) { | ||
newStats, err := diskstats.ReadDiskstats() | ||
if err != nil { | ||
log.Warn().Err(err).Msg("Error reading disk stats from procfs.") | ||
} | ||
for dev, stats := range newStats { | ||
if _, ok := devices[dev]; !ok { | ||
devices[dev] = newDevice(dev) | ||
} | ||
devices[dev].totalReads.update(stats, delta) | ||
devices[dev].totalWrites.update(stats, delta) | ||
devices[dev].readRate.update(stats, delta) | ||
devices[dev].writeRate.update(stats, delta) | ||
go func(d string) { | ||
sensorCh <- devices[d].totalReads | ||
}(dev) | ||
go func(d string) { | ||
sensorCh <- devices[d].totalWrites | ||
}(dev) | ||
go func(d string) { | ||
sensorCh <- devices[d].readRate | ||
}(dev) | ||
go func(d string) { | ||
sensorCh <- devices[d].writeRate | ||
}(dev) | ||
} | ||
} | ||
go helpers.PollSensors(ctx, diskIOstats, 5*time.Second, time.Second*1) | ||
go func() { | ||
defer close(sensorCh) | ||
<-ctx.Done() | ||
log.Debug().Msg("Stopped disk IO sensors.") | ||
}() | ||
return sensorCh | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
// Copyright (c) 2024 Joshua Rich <[email protected]> | ||
// | ||
// This software is released under the MIT License. | ||
// https://opensource.org/licenses/MIT | ||
|
||
package diskstats | ||
|
||
import ( | ||
"os" | ||
"slices" | ||
"strconv" | ||
"strings" | ||
|
||
"github.com/rs/zerolog/log" | ||
) | ||
|
||
//go:generate stringer -type=DiskStat -output diskStatStrings.go -linecomment | ||
const ( | ||
TotalReads DiskStat = iota + 3 // Total reads completed | ||
TotalReadsMerged // Total reads merged | ||
TotalSectorsRead // Total sectors read | ||
TotalTimeReading // Total milliseconds spent reading | ||
TotalWrites // Total writes completed | ||
TotalWritesMerged // Total writes merged | ||
TotalSectorsWritten // Total sectors written | ||
TotalTimeWriting // Total milliseconds spent writing | ||
ActiveIOs // I/Os currently in progress | ||
ActiveIOTime // Milliseconds elapsed spent doing I/Os | ||
ActiveIOTimeWeighted // Milliseconds elapsed spent doing I/Os (weighted) | ||
TotalDiscardsCompleted // Total discards completed | ||
TotalDiscardsMerged // Total discards merged | ||
TotalSectorsDiscarded // Total sectors discarded | ||
TotalTimeDiscarding // Total milliseconds spent discarding | ||
TotalFlushRequests // Total flush requests completed | ||
TotalTimeFlushing // Total milliseconds spent flushing | ||
) | ||
|
||
type DiskStat int | ||
|
||
func ReadDiskstats() (map[string]map[DiskStat]uint64, error) { | ||
data, err := os.ReadFile("/proc/diskstats") | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
stats := make(map[string]map[DiskStat]uint64) | ||
lines := strings.Split(string(data), "\n") | ||
for _, line := range lines[:len(lines)-1] { | ||
fields := strings.Split(line, " ") | ||
fields = slices.DeleteFunc(fields, func(n string) bool { | ||
return n == "" | ||
}) | ||
device := fields[2] | ||
stats[device] = make(map[DiskStat]uint64) | ||
for i, f := range fields { | ||
if i < 3 { | ||
continue | ||
} | ||
stat := DiskStat(i) | ||
readVal, err := strconv.ParseUint(f, 10, 64) | ||
if err != nil { | ||
log.Warn(). | ||
Err(err). | ||
Str("stat", stat.String()). | ||
Str("device", device). | ||
Msg("Unable to read disk stat.") | ||
} | ||
stats[device][stat] = readVal | ||
} | ||
} | ||
return stats, nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
// Copyright (c) 2024 Joshua Rich <[email protected]> | ||
// | ||
// This software is released under the MIT License. | ||
// https://opensource.org/licenses/MIT | ||
|
||
package main | ||
|
||
import ( | ||
"github.com/davecgh/go-spew/spew" | ||
"github.com/rs/zerolog/log" | ||
|
||
diskstats "github.com/joshuar/go-hass-agent/pkg/linux/proc" | ||
) | ||
|
||
func main() { | ||
stats, err := diskstats.ReadDiskstats() | ||
if err != nil { | ||
log.Fatal().Err(err).Msg("Could not read.") | ||
} | ||
spew.Dump(stats) | ||
} |