diff --git a/bin/riemann-hwmon b/bin/riemann-hwmon new file mode 100755 index 00000000..d895a9a4 --- /dev/null +++ b/bin/riemann-hwmon @@ -0,0 +1,8 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +Process.setproctitle($PROGRAM_NAME) + +require 'riemann/tools/hwmon' + +Riemann::Tools::Hwmon.run diff --git a/lib/riemann/tools/hwmon.rb b/lib/riemann/tools/hwmon.rb new file mode 100644 index 00000000..5b0f06a6 --- /dev/null +++ b/lib/riemann/tools/hwmon.rb @@ -0,0 +1,131 @@ +# frozen_string_literal: true + +require 'riemann/tools' + +# See https://www.kernel.org/doc/html/latest/hwmon/index.html +module Riemann + module Tools + class Hwmon + include Riemann::Tools + + class Device + attr_reader :hwmon, :type, :number, :crit, :lcrit, :label, :name + + def initialize(hwmon, type, number) + @hwmon = hwmon + @type = type + @number = number + + @crit = scale(read_hwmon_i('crit')) + @lcrit = scale(read_hwmon_i('lcrit')) + @label = read_hwmon_s('label') + @name = read_hwmon_file('name') + end + + def input + read_hwmon_i('input') + end + + def report + value = scale(input) + + state = :ok + state = :critical if crit && value >= crit + state = :critical if lcrit && value <= lcrit + { + service: "hwmon #{name} #{label}", + state: state, + metric: value, + description: fromat_input(value), + } + end + + private + + def scale(value) + return nil if value.nil? + + case type + when :fan then value.to_i # rpm + when :in, :temp, :curr then value.to_f / 1000 # mV, m°C, mA + when :humidity then value.to_f / 100 # %H + when :power, :energy then value.to_f / 1_000_000 # uW, uJ + end + end + + def fromat_input(value) + case type + when :in then format('%.3f V', { value: value }) + when :fan then "#{value} RPM" + when :temp then format('%.3f °C', { value: value }) + when :curr then format('%.3f A', { value: value }) + when :power then format('%.3f W', { value: value }) + when :energy then format('%.3f J', { value: value }) + when :humidity then format('%d %H', { value: (value * 100).to_i }) + end + end + + def read_hwmon_i(file) + s = read_hwmon_s(file) + return nil if s.nil? + + s.to_i + end + + def read_hwmon_s(file) + read_hwmon_file("#{type}#{number}_#{file}") + end + + def read_hwmon_file(file) + File.read("/sys/class/hwmon/hwmon#{hwmon}/#{file}").chomp + rescue Errno::ENOENT + nil + end + end + + FIRST_NUMBER = { + in: 0, + fan: 1, + temp: 1, + curr: 1, + power: 1, + energy: 1, + humidity: 1, + }.freeze + + attr_reader :devices + + def initialize + super + + @devices = poll_devices + end + + def poll_devices + res = [] + + hwmon = 0 + while File.exist?("/sys/class/hwmon/hwmon#{hwmon}") + %i[in fan temp curr power energy humidity].each do |type| + number = FIRST_NUMBER[type] + while File.exist?("/sys/class/hwmon/hwmon#{hwmon}/#{type}#{number}_input") + res << Device.new(hwmon, type, number) + + number += 1 + end + end + + hwmon += 1 + end + + res + end + + def tick + devices.each do |device| + report(device.report) + end + end + end + end +end diff --git a/spec/riemann/tools/http_check_spec.rb b/spec/riemann/tools/http_check_spec.rb index fdc0591b..db91b046 100644 --- a/spec/riemann/tools/http_check_spec.rb +++ b/spec/riemann/tools/http_check_spec.rb @@ -3,10 +3,12 @@ require 'openssl' begin require 'rackup/handler/webrick' + RACK_HANDLER = Rackup::Handler::WEBrick rescue LoadError # XXX: Needed for Ruby 2.6 compatibility # Moved to the rackup gem in recent versions require 'rack/handler/webrick' + RACK_HANDLER = Rack::Handler::WEBrick end require 'sinatra/base' require 'webrick' @@ -108,7 +110,7 @@ def protected! Logger: WEBrick::Log.new(File.open(File::NULL, 'w')), } @server = WEBrick::HTTPServer.new(server_options) - @server.mount('/', Rack::Handler::WEBrick, TestWebserver) + @server.mount('/', RACK_HANDLER, TestWebserver) @started = false Thread.new { @server.start } Timeout.timeout(1) { sleep(0.1) until @started } @@ -260,7 +262,7 @@ def protected! SSLCertName: '/CN=example.com', } @server = WEBrick::HTTPServer.new(server_options) - @server.mount('/', Rack::Handler::WEBrick, TestWebserver) + @server.mount('/', RACK_HANDLER, TestWebserver) @started = false Thread.new { @server.start } Timeout.timeout(1) { sleep(0.1) until @started } diff --git a/spec/riemann/tools/hwmon/device_spec.rb b/spec/riemann/tools/hwmon/device_spec.rb new file mode 100644 index 00000000..352c34e4 --- /dev/null +++ b/spec/riemann/tools/hwmon/device_spec.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +require 'riemann/tools/hwmon' + +RSpec.describe Riemann::Tools::Hwmon::Device do + subject { described_class.new(1, :temp, 2) } + + before do + allow(File).to receive(:read).with('/sys/class/hwmon/hwmon1/name').and_return("i350bb\n") + allow(File).to receive(:read).with('/sys/class/hwmon/hwmon1/temp2_input').and_return("#{input}\n") + allow(File).to receive(:read).with('/sys/class/hwmon/hwmon1/temp2_crit').and_return("96000\n") + allow(File).to receive(:read).with('/sys/class/hwmon/hwmon1/temp2_lcrit').and_raise(Errno::ENOENT) + allow(File).to receive(:read).with('/sys/class/hwmon/hwmon1/temp2_label').and_return("loc1\n") + end + + describe '#report' do + context 'when temperature is ok' do + let(:input) { 31_000 } + + it { expect(subject.report).to eq({ service: 'hwmon i350bb loc1', state: :ok, metric: 31.0, description: '31.000 °C' }) } + end + + context 'when temperature is critical' do + let(:input) { 96_000 } + + it { expect(subject.report).to eq({ service: 'hwmon i350bb loc1', state: :critical, metric: 96.0, description: '96.000 °C' }) } + end + end +end