diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 76d0925..747bd04 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -17,23 +17,24 @@ jobs: os: [ubuntu-latest] steps: - - - name: Set up Go ${{ matrix.go_version }} - uses: actions/setup-go@v6 - with: - go-version: ${{ matrix.go_version }} - - - name: Check out code into the Go module directory - uses: actions/checkout@v6 - - - name: Get dependencies - run: | - go get -v -t -d ./... - - - name: Build - run: go build -v ./... - - - name: Test - run: "go test -v -race -short ./..." - shell: bash + - name: Check out code into the Go module directory + uses: actions/checkout@v7 + + - name: Set up Go ${{ matrix.go_version }} + uses: actions/setup-go@v6 + with: + go-version: ${{ matrix.go_version }} + check-latest: true + cache: true + + - name: Get dependencies + run: | + go get -v -t -d ./... + + - name: Build + run: go build -v ./... + + - name: Test + run: "go test -v -race -short ./..." + shell: bash diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 939cd71..cde5ec6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -10,22 +10,22 @@ jobs: goreleaser: runs-on: ubuntu-latest steps: - - - name: Checkout - uses: actions/checkout@v6 - - - name: Unshallow + - name: Checkout + uses: actions/checkout@v7 + + - name: Unshallow run: git fetch --prune --unshallow - - - name: Set up Go + + - name: Set up Go uses: actions/setup-go@v6 with: go-version: 1.26.x - - - name: Run GoReleaser - uses: goreleaser/goreleaser-action@v4 + check-latest: true + + - name: Run GoReleaser + uses: goreleaser/goreleaser-action@v7 with: - version: latest + version: '~> v2' args: release --clean env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/cfg/config.go b/cfg/config.go index de3fb8d..d1e3f8f 100644 --- a/cfg/config.go +++ b/cfg/config.go @@ -11,7 +11,8 @@ import ( // Config from the file type Config struct { Simulation bool `yaml:"simulation"` - Seed int64 `yaml:"simulation_seed"` + Seed1 uint64 `yaml:"simulation_seed1"` + Seed2 uint64 `yaml:"simulation_seed2"` DiskPowerStatus map[string]DiskPowerStatus `yaml:"disk_power_status"` Sensors map[string]Task `yaml:"sensors"` DiskPools map[string][]string `yaml:"disk_pools"` @@ -20,6 +21,7 @@ type Config struct { Tasks map[string]Task `yaml:"tasks"` Schedule map[string]Schedule `yaml:"schedule"` FanControl FanControl `yaml:"fan_control"` + Telemetry Telemetry `yaml:"telemetry"` } type DiskPowerStatus struct { @@ -112,6 +114,15 @@ type SetFromTo struct { To int `yaml:"to"` } +type Telemetry struct { + Prometheus Prometheus `yaml:"prometheus"` +} + +type Prometheus struct { + Enabled bool `yaml:"enabled"` + Listen string `yaml:"listen_address"` +} + // LoadFileConfig loads the configuration from the file func LoadFileConfig(fileName string) (Config, error) { if !filepath.IsAbs(fileName) { diff --git a/config.yaml b/config.yaml index 9b28f99..db750ea 100644 --- a/config.yaml +++ b/config.yaml @@ -180,3 +180,8 @@ schedule: when: - startup - every 5m + +telemetry: + prometheus: + enabled: true + listen_address: ":8080" diff --git a/flags.go b/flags.go index b40a592..e5ffaa1 100644 --- a/flags.go +++ b/flags.go @@ -11,7 +11,8 @@ type Flags struct { verbose bool debug bool simulation bool - seed int64 + seed1 uint64 + seed2 uint64 } var ( @@ -24,5 +25,6 @@ func init() { flag.BoolVar(&flags.verbose, "v", false, "verbose - display debugging information") flag.BoolVar(&flags.debug, "d", false, "debug - display full debugging information") flag.BoolVar(&flags.simulation, "s", false, "simulation mode - test your rules with simulated sensors") - flag.Int64Var(&flags.seed, "r", 42, "random number seed to use in simulation mode") + flag.Uint64Var(&flags.seed1, "r1", 42, "random number seed to use in simulation mode") + flag.Uint64Var(&flags.seed2, "r2", 42, "random number seed to use in simulation mode") } diff --git a/go.mod b/go.mod index 65195a8..0058a6d 100644 --- a/go.mod +++ b/go.mod @@ -1,21 +1,39 @@ module github.com/creativeprojects/hardware-events -go 1.26.1 +go 1.26.5 require ( github.com/coreos/go-systemd/v22 v22.7.0 github.com/creativeprojects/clog v0.14.0 + github.com/prometheus/client_golang v1.23.2 github.com/stretchr/testify v1.11.1 + go.opentelemetry.io/otel/exporters/prometheus v0.66.0 + go.opentelemetry.io/otel/metric v1.44.0 + go.opentelemetry.io/otel/sdk/metric v1.44.0 gopkg.in/yaml.v3 v3.0.1 ) require ( + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/fatih/color v1.18.0 // indirect - github.com/kr/pretty v0.3.1 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/google/uuid v1.6.0 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/rogpeppe/go-internal v1.14.1 // indirect - golang.org/x/sys v0.42.0 // indirect + github.com/prometheus/client_model v0.6.2 // indirect + github.com/prometheus/common v0.67.5 // indirect + github.com/prometheus/otlptranslator v1.0.0 // indirect + github.com/prometheus/procfs v0.20.1 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/otel v1.44.0 // indirect + go.opentelemetry.io/otel/sdk v1.44.0 // indirect + go.opentelemetry.io/otel/trace v1.44.0 // indirect + go.yaml.in/yaml/v2 v2.4.4 // indirect + golang.org/x/sys v0.47.0 // indirect + google.golang.org/protobuf v1.36.11 // indirect ) diff --git a/go.sum b/go.sum index 9a93281..4144e58 100644 --- a/go.sum +++ b/go.sum @@ -1,31 +1,79 @@ +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/coreos/go-systemd/v22 v22.7.0 h1:LAEzFkke61DFROc7zNLX/WA2i5J8gYqe0rSj9KI28KA= github.com/coreos/go-systemd/v22 v22.7.0/go.mod h1:xNUYtjHu2EDXbsxz1i41wouACIwT7Ybq9o0BQhMwD0w= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creativeprojects/clog v0.14.0 h1:kthfMG6efS9kI4ACgYRIOPZ2efQkXvW7VhqZigCtpzU= github.com/creativeprojects/clog v0.14.0/go.mod h1:iHLlN4sZU+o5rRiFab6ZGHs2vApq09DyykMVJ2Sflro= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= +github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4= +github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw= +github.com/prometheus/otlptranslator v1.0.0 h1:s0LJW/iN9dkIH+EnhiD3BlkkP5QVIUVEoIwkU+A6qos= +github.com/prometheus/otlptranslator v1.0.0/go.mod h1:vRYWnXvI6aWGpsdY/mOT/cbeVRBlPWtBNDb7kGR3uKM= +github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc= +github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU= +go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc= +go.opentelemetry.io/otel/exporters/prometheus v0.66.0 h1:vkrK8PAznv2NKt2r+kdu252ccGzkEqLc2aSXbQIALYQ= +go.opentelemetry.io/otel/exporters/prometheus v0.66.0/go.mod h1:V/UB6D3vMF/UBOL5igAsAYnk1nG/bzYYTzvsB16cy7o= +go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc= +go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo= +go.opentelemetry.io/otel/metric/x v0.66.0 h1:YkCrx1zLOChi9ZcZ6euupOcsgzbVlec7D/xoEU1+cTA= +go.opentelemetry.io/otel/metric/x v0.66.0/go.mod h1:d1+BDj9t96do0/1LoU1ayfCv79ZgNE41qbhBvnMOBZk= +go.opentelemetry.io/otel/sdk v1.44.0 h1:nHYwb9lK+fJPU/dnT6s7W7Z8itMWyqrnVfbheVYrZ58= +go.opentelemetry.io/otel/sdk v1.44.0/go.mod h1:Osuydd3Se74nqjAKxid74N5eC+jfEqfTegHRnq58oK0= +go.opentelemetry.io/otel/sdk/metric v1.44.0 h1:3LlKgI+VjbVsjNRFZJZAJ30WjXC5VkNRks6si09iEfI= +go.opentelemetry.io/otel/sdk/metric v1.44.0/go.mod h1:5B5pMARnXxKhltooO4xUuCBorl65a4EpnTalObqOigA= +go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk= +go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ= +go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= -golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= +golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= diff --git a/lib/control.go b/lib/control.go index ba15d95..0f5cf2e 100644 --- a/lib/control.go +++ b/lib/control.go @@ -112,7 +112,7 @@ func (c *Control) Exit() error { // Start the controller. The method will return after it has kicked off the necessary goroutines. func (c *Control) Start() { for name, zone := range c.Zones { - clog.Debugf("starting %s", name) + clog.Debugf("starting zone: %s", name) zone.Start() } } diff --git a/lib/global.go b/lib/global.go index 78813dc..4beffde 100644 --- a/lib/global.go +++ b/lib/global.go @@ -1,7 +1,7 @@ package lib import ( - "math/rand" + "math/rand/v2" "os" "path/filepath" "sync" @@ -90,7 +90,7 @@ func NewGlobal(config cfg.Config) (*Global, error) { return global, err } - simulationRand := rand.New(rand.NewSource(config.Seed)) + simulationRand := rand.New(rand.NewPCG(config.Seed1, config.Seed2)) // Temperature sensors for sensorName, sensorCfg := range config.Sensors { diff --git a/lib/simulation/sensor.go b/lib/simulation/sensor.go index b3d3c3c..7563488 100644 --- a/lib/simulation/sensor.go +++ b/lib/simulation/sensor.go @@ -2,7 +2,7 @@ package simulation import ( "math" - "math/rand" + "math/rand/v2" "strconv" "sync" diff --git a/lib/simulation/sensor_test.go b/lib/simulation/sensor_test.go index 60b68e1..b58d000 100644 --- a/lib/simulation/sensor_test.go +++ b/lib/simulation/sensor_test.go @@ -1,7 +1,7 @@ package simulation import ( - "math/rand" + "math/rand/v2" "testing" "github.com/creativeprojects/hardware-events/cfg" @@ -11,7 +11,7 @@ import ( ) func TestDefaultValues(t *testing.T) { - sensor, err := NewSensor("name", cfg.Task{}, rand.New(rand.NewSource(1))) + sensor, err := NewSensor("name", cfg.Task{}, rand.New(rand.NewPCG(0, 1))) require.NoError(t, err) testData := []func(string) string{ diff --git a/lib/telemetry.go b/lib/telemetry.go new file mode 100644 index 0000000..6162c63 --- /dev/null +++ b/lib/telemetry.go @@ -0,0 +1,112 @@ +package lib + +import ( + "context" + + "go.opentelemetry.io/otel/attribute" + api "go.opentelemetry.io/otel/metric" + "go.opentelemetry.io/otel/sdk/metric" +) + +const meterName = "hardware-events" + +type Telemetry struct { + provider *metric.MeterProvider +} + +func NewTelemetry(global *Global, exporter metric.Reader) (*Telemetry, error) { + provider := metric.NewMeterProvider(metric.WithReader(exporter)) + + meter := provider.Meter(meterName) + + err := setupDisks(meter, global.Disks) + if err != nil { + return nil, err + } + + if global.FanControl != nil { + err := setupFanZones(meter, global.FanControl.Zones) + if err != nil { + return nil, err + } + } + + return &Telemetry{ + provider: provider, + }, nil +} + +func (t *Telemetry) Shutdown(ctx context.Context) error { + return t.provider.Shutdown(ctx) +} + +func setupDisks(meter api.Meter, disks map[string]*Disk) error { + _, err := meter.Int64ObservableGauge("disk_temperature", + api.WithDescription("Disk internal temperature sensor"), + api.WithUnit("degree Celsius"), + api.WithInt64Callback(func(ctx context.Context, fo api.Int64Observer) error { + for _, disk := range disks { + if disk == nil { + return nil + } + if disk.TemperatureAvailable() { + fo.Observe(int64(disk.Temperature()), api.WithAttributeSet(attribute.NewSet(diskAttributes(disk)...))) + } + } + return nil + }), + ) + if err != nil { + return err + } + + _, err = meter.Int64ObservableGauge("disk_active", + api.WithDescription("Active device: 0 when inactive, 1 when active"), + api.WithInt64Callback(func(ctx context.Context, fo api.Int64Observer) error { + for _, disk := range disks { + if disk == nil { + return nil + } + var value int64 = 0 + if disk.IsActive() { + value = 1 + } + fo.Observe(value, api.WithAttributeSet(attribute.NewSet(diskAttributes(disk)...))) + } + return nil + }), + ) + if err != nil { + return err + } + return nil +} + +func setupFanZones(meter api.Meter, zones map[string]*Zone) error { + _, err := meter.Int64ObservableGauge("fan_speed", + api.WithDescription("Fan speed from 0 to 100%"), + api.WithUnit("percent"), + api.WithInt64Callback(func(ctx context.Context, fo api.Int64Observer) error { + for _, zone := range zones { + attributes := []attribute.KeyValue{ + {Key: "name", Value: attribute.StringValue(zone.Name)}, + {Key: "id", Value: attribute.IntValue(zone.ID)}, + } + fo.Observe(int64(zone.CurrentFanSpeed()), api.WithAttributeSet(attribute.NewSet(attributes...))) + } + return nil + }), + ) + if err != nil { + return err + } + return nil +} + +func diskAttributes(disk *Disk) []attribute.KeyValue { + attributes := []attribute.KeyValue{ + {Key: "name", Value: attribute.StringValue(disk.Name)}, + {Key: "device", Value: attribute.StringValue(disk.Device)}, + } + return attributes +} diff --git a/lib/zone.go b/lib/zone.go index 0b4ce6b..295de75 100644 --- a/lib/zone.go +++ b/lib/zone.go @@ -63,7 +63,7 @@ func NewZone(sensorReader SensorReader, config cfg.FanZone, name string, setSpee sensors := make(map[string]*TemperatureSensor, len(config.Sensors)) for sensorName, sensorCfg := range config.Sensors { - sensor, err := NewTemperatureSensor(sensorCfg, sensorName, timer, sensorReader(sensorName), zone.RequestSpeed) + sensor, err := NewTemperatureSensor(sensorCfg, sensorName, timer, sensorReader(sensorName), zone.RequestFanSpeed) if err != nil { return nil, err } @@ -80,7 +80,7 @@ func (z *Zone) Start() { } } -func (z *Zone) SetSpeed(speed int) { +func (z *Zone) setFanSpeed(speed int) { if speed < z.minSpeed { speed = z.minSpeed } @@ -97,7 +97,7 @@ func (z *Zone) SetSpeed(speed int) { } } -func (z *Zone) RequestSpeed(name string, speed int, min, max bool) { +func (z *Zone) RequestFanSpeed(name string, speed int, min, max bool) { if min { speed = z.minSpeed } else if max { @@ -115,5 +115,12 @@ func (z *Zone) RequestSpeed(name string, speed int, min, max bool) { speed = bid } } - z.SetSpeed(speed) + z.setFanSpeed(speed) +} + +func (z *Zone) CurrentFanSpeed() int { + z.mutex.Lock() + defer z.mutex.Unlock() + + return z.currentSpeed } diff --git a/main.go b/main.go index 34bf6e3..40bd672 100644 --- a/main.go +++ b/main.go @@ -1,12 +1,14 @@ package main import ( + "context" "flag" "fmt" "os" "os/signal" "runtime" "syscall" + "time" "github.com/creativeprojects/clog" "github.com/creativeprojects/hardware-events/cfg" @@ -55,8 +57,9 @@ func main() { config.Simulation = true } if config.Simulation { - clog.Warningf("running in simulation mode with seed = %d", flags.seed) - config.Seed = flags.seed + clog.Warningf("running in simulation mode with seeds = %d and %d", flags.seed1, flags.seed2) + config.Seed1 = flags.seed1 + config.Seed2 = flags.seed2 } global, err := lib.NewGlobal(config) @@ -95,8 +98,25 @@ func main() { global.FanControl.Start() } + closeTelemetry, err := setupTelemetry(config, global) + if err != nil { + clog.Errorf("cannot start telemetry: %v", err) + exitCode = 1 + return + } + + closeMetricsServer, err := setupMetricsServer(config) + if err != nil { + clog.Errorf("cannot start http server: %v", err) + exitCode = 1 + return + } + // wait until we're politely asked to leave <-stop + ctx, _ := context.WithTimeout(context.Background(), 30*time.Second) + closeMetricsServer(ctx) + closeTelemetry(ctx) signal.Stop(stop) _ = global.FanControl.Exit() notifyLeaving() diff --git a/metrics.go b/metrics.go new file mode 100644 index 0000000..0486121 --- /dev/null +++ b/metrics.go @@ -0,0 +1,36 @@ +package main + +import ( + "context" + "errors" + "net/http" + "sync" + + "github.com/creativeprojects/clog" + "github.com/creativeprojects/hardware-events/cfg" + "github.com/prometheus/client_golang/prometheus/promhttp" +) + +func setupMetricsServer(config cfg.Config) (func(context.Context) error, error) { + if !config.Telemetry.Prometheus.Enabled { + return func(_ context.Context) error { return nil }, nil + } + clog.Debugf("serving metrics at %s/metrics", config.Telemetry.Prometheus.Listen) + http.Handle("/metrics", promhttp.Handler()) + server := http.Server{ + Addr: config.Telemetry.Prometheus.Listen, + } + wg := new(sync.WaitGroup) + wg.Go(func() { + if err := server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { + clog.Errorf("metrics server: %s", err) + } + clog.Debug("metrics server closed") + }) + + return func(ctx context.Context) error { + err := server.Shutdown(ctx) + wg.Wait() + return err + }, nil +} diff --git a/telemetry.go b/telemetry.go new file mode 100644 index 0000000..0e62644 --- /dev/null +++ b/telemetry.go @@ -0,0 +1,24 @@ +package main + +import ( + "context" + + "github.com/creativeprojects/hardware-events/cfg" + "github.com/creativeprojects/hardware-events/lib" + "go.opentelemetry.io/otel/exporters/prometheus" +) + +func setupTelemetry(config cfg.Config, global *lib.Global) (func(context.Context) error, error) { + if !config.Telemetry.Prometheus.Enabled { + return func(_ context.Context) error { return nil }, nil + } + exporter, err := prometheus.New() + if err != nil { + return nil, err + } + telemetry, err := lib.NewTelemetry(global, exporter) + if err != nil { + return func(_ context.Context) error { return nil }, err + } + return telemetry.Shutdown, nil +}