Azure Time Series Insights に Event Hubs 経由で Log Analytics のデータを突っ込んでみる

Azureに精通している方は、もはや何を言ってるか分からないと思いますが、Log Analyticsのグラフじゃ満足しない方や、諸事情によりLog Analyticsにしかデータを突っ込めない方が、Time Series Insightsを使いたい、という要件にお答えするエントリーです。

実際に私が構築した環境では、Functionsを利用して10分ごとにデータを突っ込んでいますが、そこは割愛します。
というのも、Functionsのコンセプト上（1つの関数で完結するのが望ましい）、Functionsに記述するソースコードが汚くなってしまうからです。
まぁ書こうと思えば書けるんでしょうけど・・・。

# 目次

- [1.読み進める前に](#1読み進める前に)
- [2.Log Analyticsからデータを引っこ抜く](#2log-analyticsからデータを引っこ抜く)
- [3.Event Hubsへデータを投げ込む](#3event-hubsへデータを投げ込む)
- [4.Time Series Insightsで見てみる](#4time-series-insightsで見てみる)
- [5.おわりに](#5おわりに)
- [ソースコード完成形](#ソースコード完成形)

# 1.読み進める前に

本エントリーはプログラムに特化しています。
Azureの各サービスの内容や構築方法、各パラメータの抽出方法等は記載いたしません。
というのも、公式HPを見ながらやってみて、特別不足しているような情報は無かったからです。
＃若干情報が古いなー、とは思いましたが。

Azureに関する情報は公式HPやMVPな方々のブログ等を見てください。
以下公式HPです。

[Azure Time Series Insights](https://azure.microsoft.com/ja-jp/services/time-series-insights/)
[Azure Event Hubs](https://azure.microsoft.com/ja-jp/services/event-hubs/)
[Azure Log Analytics](https://azure.microsoft.com/ja-jp/services/log-analytics/)

# 2.Log Analyticsからデータを引っこ抜く

兎にも角にもまずはデータを持ってこないと始まりません。
Log Analyticsには[REST API](https://dev.loganalytics.io/)が用意されており、かつ.Netライブラリも提供されているので、有効活用します。

## 2-1.ライブラリのインストール

NuGetから以下のライブラリをインストールしましょう。
依存関係で他のライブラリもインストールするけどいい？と聞かれるのでインストールしましょう。
エントリー投稿時点で利用したバージョンも記載しておきます。

```
Microsoft.Azure.OperationalInsights(0.9.0.1-preview)
Microsoft.IdentityModel.Clients.ActiveDirectory(3.19.1)
```

## 2-2.Log Analyticsへログイン

認証が必要なのでログインします。
プログラム的な表現をすると、`Log Analytics Client`を用意します。

事前にAzure ADにService Principalを登録しておく必要があります。
詳細は[こちら](https://dev.loganalytics.io/)

```csharp:sample.cs
using Microsoft.Azure.OperationalInsights;
using Microsoft.Rest.Azure.Authentication;

// params
static readonly string WorkspaceId = "<Log Analytics Workspace ID>";
static readonly string ClientId = "<Azure AD Application ID>";
static readonly string ClientSecret = "<Azure AD Application Token>";
static readonly string TenantId = "<Azure Tenant ID>";
static readonly ActiveDirectoryServiceSettings AdSettings = new ActiveDirectoryServiceSettings
{
    AuthenticationEndpoint = new Uri("https://login.microsoftonline.com"),
    TokenAudience = new Uri("https://api.loganalytics.io/"),
    ValidateAuthority = true
};

static readonly OperationalInsightsDataClient LogAnalyticsClient = LoginLogAnalytics();
// プロパティ化してもいい
// static OperationalInsightsDataClient LogAnalyticsClient => LoginLogAnalytics();

static OperationalInsightsDataClient LoginLogAnalytics()
{
    var cred = ApplicationTokenProvider.LoginSilentAsync(TenantId, ClientId, ClientSecret, AdSettings).GetAwaiter().GetResult();
    return new OperationalInsightsDataClient(cred)
    {
        WorkspaceId = WorkspaceId
    };
}
```

## 2-3.データを引っこ抜く

クライアントが用意できたらデータを引っこ抜くだけです。
引っこ抜く方法は、Log Analyticsを利用している方なら良くご存知のアレです。
Log Analytics Queryです。

詳細は省きますが、SQL文のようなQueryでいろんなことができます。

ここで注意点。
引っこ抜いたデータは、Event Hubsに渡し、最終的にTime Series Insightsに流れます。
つまり、Time Series Insightsがサポートする形式でないと、期待したグラフに描画されません。

Microsoftさんはちゃんと公式ドキュメントでサポートしている形式を公表しています。
[イベント ハブを使用して Time Series Insights 環境にイベントを送信する - サポートされている JSON 構造](https://docs.microsoft.com/ja-jp/azure/time-series-insights/time-series-insights-send-events#supported-json-shapes)

というわけで、このサポートされているJSON構造になるようにQueryを書きましょう。
ベストプラクティス、という程のものではありませんが、私はEvent Hubsに流すソースを書く前に、JSON変換して中身を確認しました。

以降のコードは、下記JSON構造のデータを取り扱う前提です。
（公式ドキュメントの「サンプル2」）

```json:sample.json
[
    {
        "id":"device1",
        "timestamp":"2016-01-08T01:08:00Z"
    },
    {
        "id":"device2",
        "timestamp":"2016-01-17T01:17:00Z"
    }
]
```

C#的には以下の型になります。
ただ動かすということであればinterfaceでもclassでもどちらでもいいです。

```csharp
IEnumelable<IDictionary<string, string>>
IEnumelable<Dictionary<string, string>>
List<IDictionary<string, string>>
List<Dictionary<string, string>>
```

さて、本題。
データを引っこ抜くメソッドを用意しましょう。

```csharp:sample.cs
using System.Collections.Generic;
using Microsoft.Azure.OperationalInsights;

static IEnumerable<IDictionary<string, string>> GetQueryResult(string query)
{
    return LogAnalyticsClient.Query(query).Results;
}
```

＃これだけなのでメソッド化しなくても良いですが、拡張性を考慮して。

そして、JSON化しましょう。

```csharp:sample.cs
using System.Threading.Tasks;
// 事前にNuGetからインストールしておきましょう。
// 他のJSON<->.NetObjectライブラリでも可。
using Newtonsoft.Json;

// params
static readonly string QueryString = "<Log Analytics Query>";

// Event Hubsに渡す時に非同期処理となるのでTask型にしておく。
static async Task Execute()
{
    var json = JsonConvert.SerializeObject(GetQueryResult(QueryString));
    // JSON文字列が入る。
}
```

これで晴れてLog Analyticsからデータを引っこ抜けました。
ここまで来ればあとは簡単です。

# 3.Event Hubsへデータを投げ込む

次はEvent Hubsです。
ログインして投げ込みます。

```csharp:sample.cs
using System.Text;
using System.Threading.Tasks;
// 事前にNuGetからインストールしておきましょう。もちろん依存関係上必要なものも。
using Microsoft.Azure.EventHubs;

static readonly string EventHubConnectionString = "<Event Hubs Connection String>";
static readonly string EventHubEntityName = "<Event Hubs Entity Name>";

static async Task Execute()
{
    try
    {
        var json = JsonConvert.SerializeObject(GetQueryResult(QueryString));
        await Client.SendAsync(new EventData(Encoding.UTF8.GetBytes(json)));
        await Client.CloseAsync();
    }
    catch (Exception e)
    {
        Console.WriteLine(e.Message);
    }
}

static readonly EventHubClient Client = GetEventHubEntity();
// プロパティ化してもいい
// static EventHubClient Client => GetEventHubEntity();

static EventHubClient GetEventHubEntity()
{
    var csb = new EventHubsConnectionStringBuilder(EventHubConnectionString)
    {
        EntityPath = EventHubEntityName
    };
    return EventHubClient.CreateFromConnectionString(csb.ToString());
}
```

エラーが出なければOKです。

# 4.Time Series Insightsで見てみる

Time Series Insightsコンソールに入ると、イベントの数でグラフが描画されます。
適宜直して、期待したとおりのグラフが描画されるか確認しましょう。

# 5.おわりに

どうでしょうか。
意外と簡単かも？と思われた方が多いかと思います。

Log Analytics SDKが思いのほかしっかりしていて、JSON化しやすい型で返してくれるのが大きいですね。

結局のところ一番大変なのは、サポートされているJSON構造にすること、だと思います。
私も実際コレが大変だと感じました。

複数のLog Analytics Queryを1つのJSONにまとめる、ということをやってものすごく苦労しました。
Time Series InsightsはTimestampフィールドがキモなので、階層が深くなった場合に、Timestampフィールドをどのレベルの階層に置くか、が重要なのです。

例として2つのQueryから得られるデータを、以下の様なJSON構造にまとめると、Time Series Insightsでは期待したグラフは描画されません。

```json:sample.json
{
    "query1": [
        {
            "data": "100",
            "timestamp": "2018-05-17T00:00:00Z"
        },
        {
            "data": "200",
            "timestamp": "2018-05-17T01:00:00Z"
        },
        ...
    ],
    "query2": [
        {
            "data": "10",
            "timestamp": "2018-05-17T00:00:00Z"
        },
        {
            "data": "20",
            "timestamp": "2018-05-17T01:00:00Z"
        },
        ...
    ]
}
```

理由は、Timestampフィールドが2種類になってしまうためです。

```
query1.timestamp
query2.timestamp
```

解決策はいくつかありますが、本エントリーの主旨から外れるので記載しません。

長くなりましたが、参考にしていただけると幸いです。
良きAzureライフを。

# ソースコード完成形

```csharp:sample.cs
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Azure.EventHubs;
using Microsoft.Azure.OperationalInsights;
using Microsoft.Rest.Azure.Authentication;
using Newtonsoft.Json;

namespace Sample
{
    class Program
    {
        // パラメータは設定ファイルから読み込む形式にするのが望ましい。
        // Log Analytics Query Parameter
        static readonly string QueryString = "<Log Analytics Query>";
        // Log Analytics Client Parameter
        static readonly string WorkspaceId = "<Log Analytics Workspace ID>";
        static readonly string ClientId = "<Azure AD Application ID>";
        static readonly string ClientSecret = "<Azure AD Application Token>";
        static readonly string TenantId = "<Azure Tenant ID>";
        static readonly ActiveDirectoryServiceSettings AdSettings = new ActiveDirectoryServiceSettings
        {
            AuthenticationEndpoint = new Uri("https://login.microsoftonline.com"),
            TokenAudience = new Uri("https://api.loganalytics.io/"),
            ValidateAuthority = true
        };
        // Event Hubs Client Parameter
        static readonly string EventHubConnectionString = "<Event Hubs Connection String>";
        static readonly string EventHubEntityName = "<Event Hubs Entity Name>";

        static void Main(string[] args)
        {
            Execute().GetAwaiter().GetResult();
        }

        static async Task Execute()
        {
            try
            {
                var json = JsonConvert.SerializeObject(GetQueryResult(QueryString));
                await Client.SendAsync(new EventData(Encoding.UTF8.GetBytes(json)));
                await Client.CloseAsync();
            }
            catch (Exception e)
            {
                Console.WriteLine(e.Message);
            }
        }

        static IEnumerable<IDictionary<string, string>> GetQueryResult(string query)
        {
            return LogAnalyticsClient.Query(query).Results;
        }

        static readonly OperationalInsightsDataClient LogAnalyticsClient = LoginLogAnalytics();

        static OperationalInsightsDataClient LoginLogAnalytics()
        {
            var cred = ApplicationTokenProvider.LoginSilentAsync(TenantId, ClientId, ClientSecret, AdSettings).GetAwaiter().GetResult();
            return new OperationalInsightsDataClient(cred)
            {
                WorkspaceId = WorkspaceId
            };
        }

        static readonly EventHubClient Client = GetEventHubEntity();

        static EventHubClient GetEventHubEntity()
        {
            var csb = new EventHubsConnectionStringBuilder(EventHubConnectionString)
            {
                EntityPath = EventHubEntityName
            };
            return EventHubClient.CreateFromConnectionString(csb.ToString());
        }
    }
}
```