C#でWindowsマシンの状態を取得してInfluxDBとGrafanaでグラフ化する

# 概要

![1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/47146/b1d23017-deff-e44d-29c7-a3af79fcc3ed.png)

ミニPCを購入したのでそれを自宅サーバーとし、メインのデスクトップPCやミニPCの状態を計測して可視化してみました。

* Windowsのマシン状態（CPUやGPUの負荷・温度・消費電力など）をC#で計測する
* InfluxDBでログを収集する
* Grafanaを使って可視化する

という話をします。

# 実装サンプル

* https://github.com/TORISOUP/PCMonitorSample


# DockerでInfluxDBとGrafanaを起動する

## 1. Docker Desktopのインストール

Windows上でDockerを使うため、[Docker Desktop](https://www.docker.com/ja-jp/products/docker-desktop/)をインストールします。
同時にWSL2の導入も行われます。


## 2. docker composeの用意

`InfluxDB`は時系列データベースで、今回のような定期的に計測したデータを保管するサービスとして最適です。
計測したデータを`InfluxDB`に流し、それを`Grafana`というツールでグラフ化して描画することにします。


次の記事を参考に`docker-compose.yml`を構築しました。


https://qiita.com/7280ayubihs/items/ace07b14d934dca4744c


```yml:docker-compose.yml
version: "2"

services:
  influxdb:
    image: influxdb:2.7.1
    container_name: influxdb-local
    volumes:
      - C:/docker/volume/influxdb/data:/var/lib/influxdb2
      - C:/docker/volume/docker/influxdb2/config:/etc/influxdb2
    ports:
      - 8086:8086
    environment:
      - DOCKER_INFLUXDB_INIT_MODE=setup
      - DOCKER_INFLUXDB_INIT_USERNAME=admin
      - DOCKER_INFLUXDB_INIT_PASSWORD=password
      - DOCKER_INFLUXDB_INIT_ORG=organization
      - DOCKER_INFLUXDB_INIT_BUCKET=BUCKET
    restart: always

  grafana:
    image: grafana/grafana-oss:9.5.5
    container_name: grafana-local
    ports:
      - 8085:3000
    user: "472"
    volumes:
      - C:/docker/volume/grafana/data:/var/lib/grafana
    depends_on:
      - influxdb
    environment:
      - GF_SERVER_ROOT_URL=http://localhost:8085
      - GF_SECURITY_ADMIN_PASSWORD=admin
    restart: always
```

`environment`の部分で初期アカウント名や初期パスワードが指定できます。
`volumes`の部分は実際にデータが書き込まれるディレクトリになるため、容量の空いている場所を指定しておくとよいでしょう。

準備ができたら`docker-compose up`でコンテナを起動します。

![4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/47146/055ae56c-2ce3-2c61-dd26-a5b05e7131e7.png)


コンテナが正しく起動したら次のURLにブラウザでアクセスし、各システムが立ち上がっていることを確認します。

* InfluxDB : http://localhost:8086/
* Grafana: http://localhost:8085/

![5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/47146/b2981146-d212-95f6-2dff-8dbf17c33558.png)
![6.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/47146/2fca65d1-b5af-9e67-bfec-0b612e1b834f.png)



## 3. InfluxDBの準備

* InfluxDB : http://localhost:8086/

InfluxDBにアクセスするとログイン画面が表示されます。

* Username : `DOCKER_INFLUXDB_INIT_USERNAME`で指定したもの
* Password: `DOCKER_INFLUXDB_INIT_PASSWORD`で指定したもの

上記のUsernameとPasswordでログインできるはずです。


### 3-1. Bucketを作る

ログインができたらデータを格納するための`Bucket`を作成します。
`Load Data` -> `Buckets` -> `+ CREATE BUCKET`より作成できます。

![7.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/47146/e4dbfdf7-0b11-678e-ef13-9466188539e7.png)


* `Name` : Bucketの名前。**C#からデータを流し込む時に使います。**
* `Delete Data` : データの保存期間。古いデータを自動で消してほしいなら`OLDER THAN`を指定。


### 3-2. API Tokenの作成

Bucketを作成したらアクセスするためのAPI Tokenを生成します。
`Load Data` -> `API TOKENS` -> `GENERATE API TOKEN` -> `Custom API Token` から作成できます。

アクセス範囲として、「3-1」で作成したBucketに対するWrite権を指定します。それ以上の権限は今回必要ありません。

![8.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/47146/b38a9e37-0912-c2b8-5bde-605824e7669e.png)
![9.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/47146/7122b8d0-afb9-af0b-3239-9cc110bf854c.png)


API Tokenが作成できたらこれをメモしておきます。あとで使います。


# C#でPCの状態を計測してInfluxDBに送る

続いてC#でPC状態を計測し、InfluxDBへ送れるようにします。

## 4. OpenHardwareMonitorLibを準備する

C#でWindowsの状態を計測するには`OpenHardwareMonitor`が使えます。

* [OpenHardwareMonitor](https://github.com/openhardwaremonitor/openhardwaremonitor/)

上記リポジトリをCloneして手元でビルドするか、[ビルドファイル](https://openhardwaremonitor.org/downloads/)をダウンロードし、**OpenHardwareMonitorLib.dll**を抜き取っておきます。


※ **OpenHardwareMonitorでマシン状態を取得する場合、いくつかのパラメーターは管理者権限が必要になります。**

## 5. C#プロジェクトを.NET Framework4.8で作る

`OpenHardwareMonitorLib`は.NET Framework向けのライブラリであるため、**.NET Framework 4.8**でC#プロジェクトを作成します。

![2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/47146/bf75c918-1353-9029-82b3-fa262822334f.png)

* 今回は「PcMonitor」というプロジェクト名にした
* 「Windowsアプリケーション」で作成しているが、「コンソールアプリケーション」や「Windows Forms」でも可

プロジェクトができたら、 プロジェクトの参照にさきほどの`OpenHardwareMonitorLib.dll`を追加しておきます。

```xml:PcMonitor.csproj
<ItemGroup>
    <Reference Include="OpenHardwareMonitorLib">
    <HintPath> <!--OpenHardwareMonitorLib.dllへのPathをここに --></HintPath>
    </Reference>
</ItemGroup>
```

## 6. 計測値を保存するデータ構造を用意する

`OpenHardwareMonitor`で取得したデータを一時保存するデータ構造を定義します。

今回は次の情報を取得することにします。

* CPU
    * パッケージ温度
    * 消費電力
    * 使用率
* GPU
    * コア温度
    * 消費電力
    * 使用率
* メモリ
    * 使用率


これらを格納するデータ構造を定義します。

```cs
using System;

namespace PcMonitor.Protocol
{
    // マシン情報
    [Serializable]
    public class MachineInfo
    {
        public CpuInfo Cpu;
        public MemoryInfo Memory;
        public GpuInfo Gpu;
        
        public MachineInfo(CpuInfo cpu, MemoryInfo memory, GpuInfo gpu)
        {
            Cpu = cpu;
            Memory = memory;
            Gpu = gpu;
        }
        
        public override string ToString()
        {
            return $"{nameof(Cpu)}: {Cpu}, {nameof(Memory)}: {Memory}, {nameof(Gpu)}: {Gpu}";
        }
    }

    // CPU状態
    [Serializable]
    public class CpuInfo
    {
        public string Name;
        public Element Temperature;
        public Element Load;
        public Element Watt;

        public CpuInfo(string name, Element temperature, Element load, Element watt)
        {
            Name = name;
            Temperature = temperature;
            Load = load;
            Watt = watt;
        }
        
        public override string ToString()
        {
            return $"{nameof(Name)}: {Name}, {nameof(Temperature)}: {Temperature}, {nameof(Load)}: {Load}, {nameof(Watt)}: {Watt}";
        }
    }

    // メモリ状態
    [Serializable]
    public class MemoryInfo
    {
        public string Name;
        public Element Load;

        public MemoryInfo(string name, Element load)
        {
            Name = name;
            Load = load;
        }

        public override string ToString()
        {
            return $"{nameof(Name)}: {Name}, {nameof(Load)}: {Load}";
        }
    }

    // GPU状態
    [Serializable]
    public class GpuInfo
    {
        public string Name;
        public Element Temperature;
        public Element Load;
        public Element Watt;
        
        public GpuInfo(string name, Element temperature, Element load, Element watt)
        {
            Name = name;
            Temperature = temperature;
            Load = load;
            Watt = watt;
        }
        
        public override string ToString()
        {
            return $"{nameof(Name)}: {Name}, {nameof(Temperature)}: {Temperature}, {nameof(Load)}: {Load}, {nameof(Watt)}: {Watt}";
        }
    }

    // 各要素の数値
    [Serializable]
    public class Element
    {
        public string Type;
        public string Target;
        public float? Max;
        public float? Current;

        public Element(string type, string target, float? max, float? current)
        {
            Type = type;
            Target = target;
            Max = max;
            Current = current;
        }

        public override string ToString()
        {
            return
                $"{nameof(Type)}: {Type}, {nameof(Target)}: {Target}, {nameof(Max)}: {Max}, {nameof(Current)}: {Current}";
        }
    }
}
```

当初はjsonで書き出すことを想定していたのでそれを意識した構造になっています。`Serializable`なのはその名残です。
このあたりのデータ構造は自分で扱いやすいように定義すればOKです。


## 7. OpenHardwareMonitorでデータを取得する

`OpenHardwareMonitor`を用いてマシン情報を取得できるようにします。
基本的な使い方は次です。

1. `OpenHardwareMonitor.Hardware.Computer`を作成
2. `Computer.Open()`を呼び出す
3. 必要な `IHardware` を取得する
4. `IHardware` から各種数値を取得する

```cs
using System;
using System.Linq;
using OpenHardwareMonitor.Hardware;
using PcMonitor.Protocol;

namespace PcMonitor
{
    // マシン状態の取得機構
    public sealed class HardwareObserver : IDisposable
    {
        private readonly Computer _computer = new();

        private readonly IHardware? _cpu;
        private readonly IHardware? _memory;
        private readonly IHardware? _gpu;

        public HardwareObserver()
        {
            // OpenHardwareMonitorの初期化
            _computer.Open();
            
            // CPU, メモリ, GPUの情報を取得する
            _computer.CPUEnabled = true;
            _computer.RAMEnabled = true;
            _computer.GPUEnabled = true;

            // Computer.Hardwareに取得したデバイス情報が詰まっている
            // ここからCPU, メモリ, GPUのデバイス情報を抜き取っておく
            foreach (var hardware in _computer.Hardware)
            {
                switch (hardware.HardwareType)
                {
                    case HardwareType.CPU:
                        _cpu = hardware;
                        break;
                    case HardwareType.RAM:
                        _memory = hardware;
                        break;
                    case HardwareType.GpuNvidia:
                    case HardwareType.GpuAti:
                        _gpu = hardware;
                        break;
                }
            }
        }

        /// <summary>
        /// マシン状態を取得する
        /// </summary>
        public MachineInfo GetMachineInfo()
        {
            // 最新の状態に更新する
            _cpu?.Update();
            _memory?.Update();
            _gpu?.Update();

            var cpuInfo = GetCpuInfo();
            var memoryInfo = GetMemoryInfo();
            var gpuInfo = GetGpuInfo();

            return new MachineInfo(cpuInfo, memoryInfo, gpuInfo);
        }

        /// <summary>
        /// GPU状態の取得
        /// </summary>
        private GpuInfo? GetGpuInfo()
        {
            if (_gpu == null) return null;

            // GPU温度
            var gpuTemperature = _gpu.Sensors.FirstOrDefault(sensor =>
                sensor.SensorType == SensorType.Temperature && sensor.Name == "GPU Core");
            // 負荷
            var gpuLoad = _gpu.Sensors.FirstOrDefault(sensor => sensor.SensorType == SensorType.Load
                                                                && sensor.Name == "GPU Core");
            // 消費電力
            var gpuPower = _gpu.Sensors.FirstOrDefault(sensor => sensor.SensorType == SensorType.Power
                                                                 && sensor.Name == "GPU Power");

            var gpuInfo = new GpuInfo(
                _gpu.Name,
                new Element("Temperature", gpuTemperature?.Name
                    , gpuTemperature?.Max, gpuTemperature?.Value),
                new Element("Load", gpuLoad?.Name, gpuLoad?.Max, gpuLoad?.Value),
                new Element("Watt", gpuPower?.Name, gpuPower?.Max, gpuPower?.Value)
            );

            return gpuInfo;
        }

        /// <summary>
        /// メモリ情報の取得
        /// </summary>
        private MemoryInfo? GetMemoryInfo()
        {
            if (_memory == null) return null;

            // メモリ使用率
            var memoryLoad = _memory.Sensors.FirstOrDefault(sensor => sensor.SensorType == SensorType.Load);

            var memoryInfo = new MemoryInfo(
                _memory.Name,
                new Element("Memory Load", _memory.Name, memoryLoad?.Max, memoryLoad?.Value)
            );
            return memoryInfo;
        }

        /// <summary>
        /// CPU情報の取得
        /// </summary>
        private CpuInfo? GetCpuInfo()
        {
            if (_cpu == null) return null;

            // CPU温度
            var cpuTemperature = _cpu.Sensors.FirstOrDefault(sensor =>
                sensor.SensorType == SensorType.Temperature && sensor.Name == "CPU Package");

            // CPU負荷
            var cpuLoad = _cpu.Sensors.FirstOrDefault(sensor => sensor.SensorType == SensorType.Load
                                                                && sensor.Name == "CPU Total");
            // CPU消費電力
            var cpuWatt = _cpu.Sensors.FirstOrDefault(sensor => sensor.SensorType == SensorType.Power
                                                                && sensor.Name == "CPU Package");

            var cpuInfo = new CpuInfo(
                _cpu.Name,
                new Element("Temperature", cpuTemperature?.Name
                    , cpuTemperature?.Max, cpuTemperature?.Value),
                new Element("Load", cpuLoad?.Name, cpuLoad?.Max, cpuLoad?.Value),
                new Element("Watt", cpuWatt?.Name, cpuWatt?.Max, cpuWatt?.Value)
            );
            return cpuInfo;
        }

        public void Dispose()
        {
            _computer.Close();
        }
    }
}
```

```cs:使い方
using System;
using System.Threading;
using System.Threading.Tasks;
using PcMonitor;

// HardwareObserverの作成
using var hardwareObserver = new HardwareObserver();

// 1秒ごとにマシン情報を取得して表示する
while (true)
{
    var machineInfo = hardwareObserver.GetMachineInfo();
    Console.WriteLine(machineInfo);

    await Task.Delay(TimeSpan.FromSeconds(1));
}
```


#### 補足

実際にどのパラメーターが取れるのかは使用しているマシン構成によって変わります。
Open Hardware Monitorのexeを起動することで実際に計測できる数値を列挙できるので、これを参考に処理を組み立てるとよいでしょう。

![3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/47146/cc7c863d-8f29-65ee-05d8-51e00074dcb8.png)


## 8. InfluxDB.Clientを導入する

C#のInfluxDBクライアントとして`InfluxDB.Client`を使います。NuGetでプロジェクトへインストールします。

* [NuGet - InfluxDB.Client](https://www.nuget.org/packages/InfluxDB.Client/)

```xml:csproj
    <ItemGroup>
      <PackageReference Include="InfluxDB.Client" Version="4.18.0" />
    </ItemGroup>
```

## 9. InfluxDBへの書き込み機構を作る

`InfluxDBClient`を使ってInfluxDBへ計測データを書き込むようにします。
書き込み方法はいくつかありますが、今回は`POCO(Plain Old CLR Object)`を使う方法にしました。



```cs
using System;
using System.Threading.Tasks;
using InfluxDB.Client;
using InfluxDB.Client.Api.Domain;
using InfluxDB.Client.Core;
using PcMonitor.Protocol;

namespace PcMonitor
{
    public sealed class InfluxDbSender : IDisposable
    {
        private readonly InfluxDBClient _influxDbClient;
        private readonly string _bucket;
        private readonly string _organization;

        public InfluxDbSender(string url, string token, string bucket, string organization)
        {
            _influxDbClient = new InfluxDBClient(url, token);
            _bucket = bucket;
            _organization = organization;
        }

        // MachineInfoをInfluxDBへ書き込む
        public async ValueTask SendMachineInfoAsync(MachineInfo machineInfo, CancellationToken ct)
        {
            try
            {
                // 非同期用のWriter取得
                var writeApi = _influxDbClient.GetWriteApiAsync();

                // CPU状態の書き込み
                if (machineInfo.Cpu != null)
                {
                    var cpu = new CPU
                    {
                        Time = DateTime.UtcNow,
                        Name = machineInfo.Cpu.Name,
                        Temperature = machineInfo.Cpu.Temperature.Current,
                        Load = machineInfo.Cpu.Load.Current,
                        Watt = machineInfo.Cpu.Watt.Current
                    };

                    await writeApi.WriteMeasurementAsync(cpu, WritePrecision.S, bucket: _bucket, org: _organization,
                        cancellationToken: ct);
                }

                // メモリ状態の書き込み
                if (machineInfo.Memory != null)
                {
                    var memory = new Memory
                    {
                        Time = DateTime.UtcNow,
                        Name = machineInfo.Memory.Name,
                        Load = machineInfo.Memory.Load.Current!
                    };
                    await writeApi.WriteMeasurementAsync(memory, WritePrecision.S, bucket: _bucket, org: _organization,
                        cancellationToken: ct);
                }

                // GPU状態の書き込み
                if (machineInfo.Gpu != null)
                {
                    var gpu = new GPU
                    {
                        Time = DateTime.UtcNow,
                        Name = machineInfo.Gpu.Name,
                        Temperature = machineInfo.Gpu.Temperature.Current!,
                        Load = machineInfo.Gpu.Load.Current!,
                        Watt = machineInfo.Gpu.Watt.Current!
                    };

                    await writeApi.WriteMeasurementAsync(gpu, WritePrecision.S, bucket: _bucket, org: _organization,
                        cancellationToken: ct);
                }
            }
            catch (Exception e)
            {
                Console.WriteLine(e);
            }
        }

        public void Dispose()
        {
            _influxDbClient.Dispose();
        }
    }
}
```

```cs
using System;
using InfluxDB.Client.Core;

// POCO
// データベースの構造をクラス化したもの
namespace PcMonitor
{
    [Measurement("CPU")]
    public class CPU
    {
        [Column(IsTimestamp = true)] public DateTime Time { get; set; }
        [Column("Name", IsTag = true)] public string Name { get; set; }
        [Column("Temperature")] public double? Temperature { get; set; }
        [Column("Load")] public double? Load { get; set; }
        [Column("Watt")] public double? Watt { get; set; }
    }

    [Measurement("Memory")]
    public class Memory
    {
        [Column(IsTimestamp = true)] public DateTime Time { get; set; }
        [Column("Name", IsTag = true)] public string Name { get; set; }
        [Column("Load")] public double? Load { get; set; }
    }

    [Measurement("GPU")]
    public class GPU
    {
        [Column(IsTimestamp = true)] public DateTime Time { get; set; }
        [Column("Name", IsTag = true)] public string Name { get; set; }
        [Column("Temperature")] public double? Temperature { get; set; }
        [Column("Load")] public double? Load { get; set; }
        [Column("Watt")] public double? Watt { get; set; }
    }

    [Measurement("Network")]
    public class Network
    {
        [Column(IsTimestamp = true)] public DateTime Time { get; set; }
        [Column("Name", IsTag = true)] public string Name { get; set; }
        [Column("SentByte")] public long Sent { get; set; }
        [Column("ReceivedByte")] public long Received { get; set; }
    }
}
```


```cs:使い方
using System;
using System.Threading;
using System.Threading.Tasks;
using PcMonitor;

// HardwareObserverの作成
using var hardwareObserver = new HardwareObserver();

// InfluxDbSenderの作成
using var influxDbSender = new InfluxDbSender(
    url: "http://localhost:8086",
    token: "3-2で作ったAPI Token",
    bucket: "3-1で作ったBucketの名前",
    organization: "DOCKER_INFLUXDB_INIT_ORGで指定したorganization名"
);


// 1秒ごとにマシン情報を取得してInfluxDBに送信
while (true)
{
    var machineInfo = hardwareObserver.GetMachineInfo();
    await influxDbSender.SendMachineInfoAsync(machineInfo, CancellationToken.None);
    await Task.Delay(TimeSpan.FromSeconds(1));
}
```

## 10.ビルドして動かしてみる

準備ができたらビルドしてexeを起動してみます。注意点として**管理者権限で起動しないとアクセスできない情報があります。**
CPUの消費電力などは管理者権限でないと取得できません。

![10.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/47146/56946585-4282-8160-59b0-4aba486aa9af.jpeg)


うまく動作していればInfluxDBのBucketにデータが書き込まれているはずです。


## 11.よしなに実装を整える

このままでも動作はしますが、Tokenなどがハードコードすぎるので外部ファイルから設定値を読み取って動くようにしてみました。

exeファイルがあるディレクトリと同じ場所にある`config.json`を読み込み、その設定値を用いて動作するようにしました。
Json.NETを使用しています。

```cs
using System;

namespace PcMonitor
{
    [Serializable]
    public class Configuration
    {
        public string? InfluxDbUrl = "http://localhost:8086";
        public string? Token = "my-token";
        public string? Organization = "my-org";
        public string? Bucket = "my-bucket";
        public int? IntervalMilliSeconds = 5000;
    }
}
```

```cs
using System;
using System.IO;
using System.Threading.Tasks;
using Newtonsoft.Json;

namespace PcMonitor.Sender
{
    public static class Program
    {
        public static async Task Main()
        {
            try
            {
                // HardwareObserverの作成
                using var hardwareObserver = new HardwareObserver();

                // Configファイルの読み込み
                var conf = GetConfiguration() ?? throw new Exception("Configuration is null");

                // Configファイルを用いて初期化
                var dbSender = new InfluxDbSender(conf.InfluxDbUrl, conf.Token, conf.Bucket, conf.Organization);

                // 転送開始
                await SendMachineInfoAsync(dbSender, hardwareObserver, conf);
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex);
            }
        }

        /// <summary>
        /// 定期的にInfluxDBにマシン情報を送信する
        /// </summary>
        private static async Task SendMachineInfoAsync(InfluxDbSender influxDbSender,
            HardwareObserver hardwareObserver,
            Configuration conf)
        {
            var retryCount = 0;

            while (true)
            {
                try
                {
                    // マシン情報を取得して送信
                    var machineInfo = hardwareObserver.GetMachineInfo();
                    await influxDbSender.SendMachineInfoAsync(machineInfo, default);

                    retryCount = 0;
                    await Task.Delay(conf.IntervalMilliSeconds ?? 5000);
                }
                catch (Exception ex)
                {
                    Console.WriteLine(ex);
                    
                    // InfluxDBへの送信に失敗した場合はしばらく待ってリトライ
                    // リトライ間隔は徐々に伸ばす
                    retryCount++;
                    if (retryCount > 20) return;
                    await Task.Delay(TimeSpan.FromMinutes(retryCount));
                }
            }
        }
        
        /// <summary>
        /// exeファイルがあるディレクトリにあるconfig.jsonを読み込む
        /// </summary>
        private static Configuration? GetConfiguration()
        {
            var appPath = System.Reflection.Assembly.GetExecutingAssembly().Location;
            var appDir = Path.GetDirectoryName(appPath);
            var path = Path.Combine(appDir, "config.json");
            if (!File.Exists(path))
            {
                // 存在しないならデフォルト値で作る
                var conf = new Configuration();
                var j = JsonConvert.SerializeObject(conf);
                File.WriteAllText(path, j);
                return conf;
            }

            var json = File.ReadAllText(path);
            return JsonConvert.DeserializeObject<Configuration>(json);
        }
    }
}
```

これも必要最低限の実装なので、必要に応じて改変してください。
`Windows Forms`とかを使ってもよいかと。

# Grafanaで可視化する

InfluxDBに書き込まれたデータをGrafanaから参照して表示できるようにしてみます。
（そもそもInfluxDB自体にグラフ化する機能があるのでGrafana不要といわれれば、そう）

## 12. InfluxDB側でRead用のAPI Tokenを作成する

InfluxDB側でGrafanaからデータを取得できるようにAPI Tokenを作成しておきます。

![12.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/47146/96dcce70-c45e-e981-c148-c721c47a1952.jpeg)


今回は`Read`側の権限を付与しておきます。




## 13. GrafanaをInfluxDBに接続する

* http://localhost:8085/

ローカルで建てたGrafanaにアクセスし、ログインします。初期IDとパスワードは両方とも`admin`です。

ログインができたら`Connections` -> `Connect data`より `Influx DB`を選択します。

![11.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/47146/b2300c91-d442-4a51-78f6-7f9a0b93ab57.jpeg)


続いて各種パラメータを設定します。

![13.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/47146/a631660c-eb2d-4b03-e77e-a8d900ce0834.jpeg)
![14.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/47146/df1f4b6e-30c9-8ec3-aca9-385ee8ef6fc8.jpeg)



* Query Language : `Flux`
* HTTP-URL : `http://influxdb-local:8086` （Dockerのネットワークを指定）
* InfluxDB Details-Organization : `DOCKER_INFLUXDB_INIT_ORGで指定したorganization名`
* InfluxDB Details-Token : `11. で作成したAPI Token`
* InfluxDB Details-Default `Bucket : Bucket名`

設定が終わったら`Save & test`を押します。InfluxDBへの接続テストが実行されるので、成功していることを確認してください。

## 14. GrafanaのDashboardsよりデータの可視化をする

![15.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/47146/49ecac68-a2b5-48d1-9977-34223fffb017.jpeg)


Dashboardの`Add Visualization`よりデータの可視化をします。

たとえばCPU温度を可視化したければ次のようなFluxクエリを指定します。

```
from(bucket: "DesktopPC")
  |> range(start: v.timeRangeStart, stop: v.timeRangeStop)
  |> filter(fn: (r) => r["_measurement"] == "CPU")
  |> filter(fn: (r) => r["_field"] == "Temperature")
  |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: true)
  |> yield(name: "mean")
```

![16.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/47146/0dd5c747-4f1c-ed7e-4740-aa2b0dac85ba.jpeg)


## 15. よしなにグラフを揃えて完成

![1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/47146/b1d23017-deff-e44d-29c7-a3af79fcc3ed.png)


完成です！

# 感想

* InfluxDBとGrafanaの構築はDocker-Composeで一瞬だった
* OpenHardwareMonitorが`.NET Framework`でしか動かないこと、管理者権限が無いと情報が全部取れないところにハマった
* いろいろ可視化すると面白くて楽しい


# おまけ

## ChatGPTにクエリを書かせると便利

InfluxDBに投げるように定義したPOCOですが、これをChatGPTにそのまま渡して指示すれば可視化用のFluxクエリを組み立ててくれます。
（そのままコピペでうまくいくこともあるが、多少の手直しは必要）

```cs
// これをそのままコピペしてChatGPTに食わせて、
// 「CPUのTemperatureをGrafanaでグラフ化するFluxクエリを作って」とか頼めば作ってくれる

[Measurement("CPU")]
public class CPU
{
    [Column(IsTimestamp = true)] public DateTime Time { get; set; }
    [Column("Name", IsTag = true)] public string Name { get; set; }
    [Column("Temperature")] public double? Temperature { get; set; }
    [Column("Load")] public double? Load { get; set; }
    [Column("Watt")] public double? Watt { get; set; }
}

[Measurement("Memory")]
public class Memory
{
    [Column(IsTimestamp = true)] public DateTime Time { get; set; }
    [Column("Name", IsTag = true)] public string Name { get; set; }
    [Column("Load")] public double? Load { get; set; }
}

[Measurement("GPU")]
public class GPU
{
    [Column(IsTimestamp = true)] public DateTime Time { get; set; }
    [Column("Name", IsTag = true)] public string Name { get; set; }
    [Column("Temperature")] public double? Temperature { get; set; }
    [Column("Load")] public double? Load { get; set; }
    [Column("Watt")] public double? Watt { get; set; }
}

[Measurement("Network")]
public class Network
{
    [Column(IsTimestamp = true)] public DateTime Time { get; set; }
    [Column("Name", IsTag = true)] public string Name { get; set; }
    [Column("SentByte")] public long Sent { get; set; }
    [Column("ReceivedByte")] public long Received { get; set; }
}
```

![17.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/47146/1a136908-4964-98a0-8c21-8b38092111e7.jpeg)


## 電気料金を出してみた

CPUとGPUの「1秒おきのW数」は取れているのでこれから１時間当たりの平均値を求め「Wh」とし、そこに電気料金をかけることで１時間あたりのだいたいの電気料金を算出してみました。

![18.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/47146/487b3d89-00f8-dedd-a5f6-423a1024d5e2.jpeg)

（23時ごろにVRで遊びだしたので電気料金が一気に上がってるのがわかる）


```
// CPUの電気料金を計算
cpuCost = from(bucket: "MonitorDesktopPC")
  |> range(start: v.timeRangeStart, stop: v.timeRangeStop)
  |> filter(fn: (r) => r["_measurement"] == "CPU")
  |> filter(fn: (r) => r["_field"] == "Watt")
  // 1時間の平均からWhを求める
  |> aggregateWindow(every: 1h, fn: mean, createEmpty: true)
  |> map(fn: (r) => ({
      _time: r._time,
      // 第二段階の電気料金(1kWhあたり)をかける
      CPU_ElectricityCost: (r._value / 1000.0) * 36.4
  }))

// GPUの電気料金を計算
gpuCost = from(bucket: "MonitorDesktopPC")
  |> range(start: v.timeRangeStart, stop: v.timeRangeStop)
  |> filter(fn: (r) => r["_measurement"] == "GPU")
  |> filter(fn: (r) => r["_field"] == "Watt")
  |> aggregateWindow(every: 1h, fn: mean, createEmpty: true)
  |> map(fn: (r) => ({
      _time: r._time,
      GPU_ElectricityCost: (r._value / 1000.0) * 36.4 
  }))

// CPUとGPUの結果を結合
join(tables: {cpu: cpuCost, gpu: gpuCost}, on: ["_time"])
  |> map(fn: (r) => ({
      _time: r._time,
      TotalElectricityCost: r.CPU_ElectricityCost + r.GPU_ElectricityCost // 合計料金を計算
  }))
  |> yield(name: "TotalElectricityCost")
```