Exporting Open Hardware Data into Graphite

grafana home Beautiful dashboards for your home components, now with GPU and power usage

Update: 2017-11-01: I’ve opensourced this utility as OhmGraphite, It can be ran as a CLI app or Windows service. The code showcased here demonstrates how to get a minimal example running. See the repo for a more production worthy approach!

Hot on the heels of Monitoring Window’s system metrics with Grafana are two fixes for the dashboard. Graphs about the GPU and power usage were missing from the dashboard, but I’m happy to state that I’ve fixed this with the help from Open Hardware Monitor, which is an open source application/library for reading sensor data from computer components. I was able to utilize this library to send this data into graphite to be displayed on my grafana dashboards!

The plan is to create a simple Windows service that will run on boot and feed data into graphite.

Open Hardware Monitor WMI

Open Hardware Monitor publishes all sensor data to WMI, but since I’m not familiar with WMI, it sounds like another layer of indirection from the hardware. I’d also like to not run the application, which I’m presuming is what is required to send to WMI. Not to mention I’d still need to write the code to export WMI metrics to graphite. Since graphite has such a simple easy API for feeding data into it, writing custom code will be the quickest way to see the data.

Open Hardware Monitor Library

I’ve mentioned that Open Hardware Monitor has a library, but did I mention that library is out of date? To get the widest range of hardware compatibility, we’ll need to work off the master branch in the source code. It’s inconvenient, but not a show stopper, as we’ll just need to reference our local build in our code.

Hardware Caveat

Open Hardware Monitor is great, but there are some holes. My Asus Z170 Pro Gaming motherboard has zero sensors registered. There looks to be an outstanding issue for this. The following pull request shows fan speeds, but not much else. I suspect there must be other hardware configurations where sensors are not detected, so user beware.

Once my motherboard support is added to the library I’ll be able to add panels showing voltages, temperatures, and fan speeds across multiple components.

The Code

Since Open Hardware Monitor is written in C#, our service will also. Our code uses some C# features, so some syntax may look new to C# veterans (I’m certainly still getting used to it).

using System;
using System.ComponentModel;
using System.Configuration.Install;
using System.IO;
using System.Net.Sockets;
using System.ServiceProcess;
using System.Threading;
using OpenHardwareMonitor.Hardware;
using Timer = System.Timers.Timer;

namespace ConsoleApp
{
    public class Program 
    {
        static void Main(string[] args)
        {
            // Run as windows service if we're not in a terminal
            if (!Environment.UserInteractive)
            {
                ServiceBase.Run(new OhmGraphite());
            }
            else
            {
                OhmGraphite.RunDaemon();
            }
        }
    }

    // Derive from `ServiceBase` so that this program can be a console app as well as a
    // windows service.
    public class OhmGraphite : ServiceBase
    {
        // token that'll be used to receive a stop event from windows services
        private static readonly CancellationTokenSource Source = new CancellationTokenSource();

        // Create a looping timer that triggers an event every 5 seconds
        private static Timer _timer = new Timer(interval: 5 * 1000) { AutoReset = true };

        public static void RunDaemon()
        {
            // We'll want to capture all available hardware metrics
            // to send to graphite
            var computer = new Computer
            {
                GPUEnabled = true,
                MainboardEnabled = true,
                CPUEnabled = true,
                RAMEnabled = true,
                FanControllerEnabled = true,
                HDDEnabled = true
            };

            // Aside: wish that the API supported `using`
            computer.Open();

            try
            {
                // Hardcode TCP connection to our local graphite server
                using (var client = new TcpClient("192.168.137.201", 2003))
                using (var networkStream = client.GetStream())
                using (var writer = new StreamWriter(networkStream))
                {
                    // Start timing
                    _timer.Enabled = true;

                    // Since this block will never finish normal execution, the writer will
                    // not be disposed under normal circumstances, so it is ok to capture the
                    // writer for our event (this would normally be bad practice because the
                    // event could be using a disposed writer)
                    _timer.Elapsed += (sender, eventArgs) => CaptureMetrics(computer, writer);

                    while (!Source.IsCancellationRequested)
                    {
                        // 2147483647ms < 25 days, and since systems can be online longer than
                        // 25 days, loop forever, sleeping 25 days at a time. No need to
                        // check for return value because the while loop checks if a
                        // cancellation has occurred
                        Source.Token.WaitHandle.WaitOne(millisecondsTimeout: int.MaxValue);
                    }

                    // Set timer to null, which will de-register all events so that the
                    // elapsed event doesn't have access to a disposed writer
                    _timer = null;
                }
            }
            finally
            {
                // When this application has been interrupted or computer shutdown, clean
                // up all resources from the usings and the computer
                computer.Close();
            }
        }

        private static void CaptureMetrics(IComputer computer, TextWriter writer)
        {
            // Grab unix timestamp at the start of the update so that all metrics
            // are reported at the same time.
            var time = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
            foreach (var hardware in computer.Hardware)
            {
                hardware.Update();
                foreach (var hardwareSensor in hardware.Sensors)
                {
                    // Take the sensor's identifier (eg. /nvidiagpu/0/load/0)
                    // and tranform into nvidiagpu.0.load.<name> where <name>
                    // is the name of the sensor lowercased with spaces removed.
                    // A name like "GPU Core" is turned into "gpucore". Also
                    // since some names are like "cpucore#2", turn them into
                    // separate metrics by replacing "#" with "."
                    var name = hardwareSensor.Identifier.ToString()
                        .Replace('/', '.')
                        .Substring(1);
                    name = name.Remove(name.LastIndexOf('.'));
                    name += '.' + hardwareSensor.Name.ToLower()
                                .Replace(" ", null).Replace('#', '.');

                    // Graphite API wants <metric> <value> <timestamp>. We prefix the metric
                    // with `ohm` as to not overwrite potentially existing metrics
                    writer.WriteLine($"ohm.{name} {hardwareSensor.Value ?? 0.0} {time:d}");
                }
            }

            // Output current to time to stdout to track progress
            Console.Out.WriteLine($"{DateTimeOffset.Now:s}");
        }

        protected override void OnStart(string[] args) => RunDaemon();
        protected override void OnStop() => Source.Cancel();
    }
}

And can’t forget the bit of code that is used to install our service:

[RunInstaller(true)]
public class OhmGraphiteInstaller : Installer
{
    public OhmGraphiteInstaller()
    {
        // Instantiate installers for process and services.
        var processInstaller = new ServiceProcessInstaller
        {
            Account = ServiceAccount.LocalSystem
        };

        var serviceInstaller1 = new ServiceInstaller
        {
            StartType = ServiceStartMode.Automatic,
            ServiceName = "ohm-graphite"
        };

        Installers.Add(serviceInstaller1);
        Installers.Add(processInstaller);
    }
}

And the command to install.

"C:\Windows\Microsoft.NET\Framework\v4.0.30319\installutil.exe" \
  C:\Apps\ohm-graphite\ConsoleApp.exe

If you want to run the code interactively, make sure to run it as administrator for CPU power and temperature.

Status

I’m calling the code “ohm-graphite”. It has the potential to be packaged as a library or application for others, but since I’m still testing the waters (I mean, I wrote the code in a couple hours). So for other graphite + grafana + windows power users (and how many those are out there?), feel free to snag the code under MIT.

Results

grafana home

Can you tell when I stopped playing video games!

Comments: