クラウドエンジニアブログ

Azure Functions から Log Analytics にログとカスタムイベントを送信してみた

佐藤 実

佐藤 実

こんにちは。 CI/CD を含むクラウドインフラの構築や技術支援を担当している、クラウドエンジニアの佐藤です。

とあるサービス開発案件で、Azure Functions を使った簡易なオンボーディングアプリを用意する事になりました。サービス提供側の要件として、オンボーディング時の情報からサービスを改善したり、KPI となる指標を算出してダッシュボードに表示する必要があります。その情報は Log Analytics に保管する予定です。

そこで今回は、Azure Functions アプリの中でプログラムが出力するログやカスタムイベントを、Application Insights 経由で Log Analytics に送信する検証をしました。


(紹介のみ)Azure Functions アプリから直接 Log Analytics に送る方法

Application Insights を経由せず、Azure Functions アプリから直接 Log Analytics に送る方法は、紹介のみで未検証です。


一つ目は、Log Analytics データコレクター API を使用する方法です。
サンプルの要求 のドキュメントを参考に Azure Functions アプリにコードを追加します。
C# のサンプルコードは下記のとおりです。

using System;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;

namespace OIAPIExample
{
  class ApiExample
  {
    // An example JSON object, with key/value pairs
    static string json = @"[{""DemoField1"":""DemoValue1"",""DemoField2"":""DemoValue2""},{""DemoField3"":""DemoValue3"",""DemoField4"":""DemoValue4""}]";

    // Update customerId to your Log Analytics workspace ID
    static string customerId = "xxxxxxxx-xxx-xxx-xxx-xxxxxxxxxxxx";

    // For sharedKey, use either the primary or the secondary Connected Sources client authentication key   
    static string sharedKey = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";

    // LogName is name of the event type that is being submitted to Azure Monitor
    static string LogName = "DemoExample";

    // You can use an optional field to specify the timestamp from the data. If the time field is not specified, Azure Monitor assumes the time is the message ingestion time
    static string TimeStampField = "";

    static void Main()
    {
      // Create a hash for the API signature
      var datestring = DateTime.UtcNow.ToString("r");
      var jsonBytes = Encoding.UTF8.GetBytes(json);
      string stringToHash = "POST\n" + jsonBytes.Length + "\napplication/json\n" + "x-ms-date:" + datestring + "\n/api/logs";
      string hashedString = BuildSignature(stringToHash, sharedKey);
      string signature = "SharedKey " + customerId + ":" + hashedString;

      PostData(signature, datestring, json);
    }

    // Build the API signature
    public static string BuildSignature(string message, string secret)
    {
      var encoding = new System.Text.ASCIIEncoding();
      byte[] keyByte = Convert.FromBase64String(secret);
      byte[] messageBytes = encoding.GetBytes(message);
      using (var hmacsha256 = new HMACSHA256(keyByte))
      {
        byte[] hash = hmacsha256.ComputeHash(messageBytes);
        return Convert.ToBase64String(hash);
      }
    }

    // Send a request to the POST API endpoint
    public static void PostData(string signature, string date, string json)
    {
      try
      {
        string url = "https://" + customerId + ".ods.opinsights.azure.com/api/logs?api-version=2016-04-01";

        System.Net.Http.HttpClient client = new System.Net.Http.HttpClient();
        client.DefaultRequestHeaders.Add("Accept", "application/json");
        client.DefaultRequestHeaders.Add("Log-Type", LogName);
        client.DefaultRequestHeaders.Add("Authorization", signature);
        client.DefaultRequestHeaders.Add("x-ms-date", date);
        client.DefaultRequestHeaders.Add("time-generated-field", TimeStampField);

        // If charset=utf-8 is part of the content-type header, the API call may return forbidden.
        System.Net.Http.HttpContent httpContent = new StringContent(json, Encoding.UTF8);
        httpContent.Headers.ContentType = new MediaTypeHeaderValue("application/json");
        Task response = client.PostAsync(new Uri(url), httpContent);

        System.Net.Http.HttpContent responseContent = response.Result.Content;
        string result = responseContent.ReadAsStringAsync().Result;
        Console.WriteLine("Return Result: " + result);
      }
      catch (Exception excep)
      {
        Console.WriteLine("API Post Exception: " + excep.Message);
      }
    }
  }
}

送信するログを JSON フォーマットにして、API シグネチャを毎回作成し、API エンドポイントに POST します。
あとは、Log Analytics のワークスペース ID とプライマリキーを Key Vault に保管して、Azure Functions の環境変数から値を読み込むようにします。


二つ目は、Log Analytics 専用のライブラリやパッケージを使う方法です。
LogAnalytics Client for .NET Core のドキュメントを参考に Azure Functions アプリに「 LogAnalytics.Client 」をインストールし、数行のコードを追加します。
C# のサンプルコードは下記のとおりです。

LogAnalyticsClient logger = new LogAnalyticsClient(
    workspaceId: "LAW ID",
    sharedKey: "LAW KEY");

await logger.SendLogEntry(new TestEntity
{
    Category = GetCategory(),
    TestString = $"String Test",
    TestBoolean = true,
    TestDateTime = DateTime.UtcNow,
    TestDouble = 2.1,
    TestGuid = Guid.NewGuid()
}, "demolog")
.ConfigureAwait(false);

「 LogAnalytics.Client 」パッケージは、先ほどの「 Log Analytics データコレクター API 」を使っているようです。
※参考: Building custom Data Collectors for Azure Log Analytics in C#

Azure Functions アプリから Application Insights 経由で Log Analytics に送る方法

Azure Functions アプリのパフォーマンスデータを収集するには、Application Insights が必要です。
そして、Application Insights の保管先は Log Analytics を選択する事ができます。
つまり、Azure Functions アプリのパフォーマンスデータを収集しつつ、Application Insights にログとカスタムイベントを送信すれば、Log Analytics にデータが自動的に保管できます。


Azure に検証環境を構築します。

## 環境変数をセットします
prefix=ceblogfunc
region=japaneast

## リソースグループを作成します
az group create \
  --name ${prefix}-rg \
  --location $region

## ストレージアカウントを作成します
az storage account create \
  --name ${prefix}stor \
  --resource-group ${prefix}-rg \
  --sku Standard_LRS

## Log Analytics を作成します
az monitor log-analytics workspace create \
  --workspace-name ${prefix}-log \
  --resource-group ${prefix}-rg

## Application Insights を作成し Log Analytics を紐づけます
az monitor app-insights component create \
  --app ${prefix}-ai \
  --location $region \
  --resource-group ${prefix}-rg \
  --workspace ${prefix}-log

## Azure Functions を作成し Application Insights とストレージアカウントを紐づけます
az functionapp create \
  --name ${prefix} \
  --resource-group ${prefix}-rg \
  --consumption-plan-location $region \
  --runtime dotnet \
  --functions-version 4 \
  --storage-account ${prefix}stor \
  --app-insights ${prefix}-ai \
  --https-only \
  --os-type Windows \
  --assign-identity


検証用の Azure Functions アプリを作成します。

## Azure Functions アプリを初期化します
func init ${prefix} --dotnet

## アプリのディレクトリに移動します
cd ${prefix}

## HTTP トリガーアプリを作成します
func new --name test --template "HTTP trigger" --authlevel "anonymous"

## ローカル環境で動作検証します
func start

## Azure Functions にアプリをデプロイします
func azure functionapp publish ${prefix}

## Azure Functions アプリにアクセスして動作確認します
curl -s https://${prefix}.azurewebsites.net/api/test


HTTP トリガーアプリ内に下記のログ出力があります。

log.LogInformation("C# HTTP trigger function processed a request.");

「 C# HTTP trigger function processed a request. 」ログを Application Insights と Log Analytics で確認します。
リアルタイムにログ出力されませんが、数分待てば下記のようにログ出力が確認できます。

Azure Functions アプリに Application Insights のカスタムイベントを追加して動作確認

Azure Functions アプリに Application Insights のカスタムイベントを送信する機能を追加します。


Azure Functions アプリの「 test.cs 」を開き、下記のようにコードを修正します。

using System;
using System.IO;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
// 下記 3 行を追加
using Microsoft.ApplicationInsights;
using Microsoft.ApplicationInsights.DataContracts;
using Microsoft.ApplicationInsights.Extensibility;

namespace ceblogfunc
{
    // クラスから static を削除
    public class test
    {
        // 下記 6 行を追加
        private readonly TelemetryClient telemetryClient;

        public test(TelemetryConfiguration telemetryConfiguration)
        {
            this.telemetryClient = new TelemetryClient(telemetryConfiguration);
        }

        [FunctionName("test")]
        // メソッドから static を削除
        public async Task<IActionResult> Run(
            [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)] HttpRequest req,
            ILogger log)
        {
            log.LogInformation("C# HTTP trigger function processed a request.");

            string name = req.Query["name"];

            string requestBody = await new StreamReader(req.Body).ReadToEndAsync();
            dynamic data = JsonConvert.DeserializeObject(requestBody);
            name = name ?? data?.name;

            // 下記 3 行を追加
            var evt = new EventTelemetry("Function called");
            evt.Context.User.Id = name;
            this.telemetryClient.TrackEvent(evt);

            string responseMessage = string.IsNullOrEmpty(name)
                ? "This HTTP triggered function executed successfully. Pass a name in the query string or in the request body for a personalized response."
                : $"Hello, {name}. This HTTP triggered function executed successfully.";

            return new OkObjectResult(responseMessage);
        }
    }
}


カスタムイベントを追加した Azure Functions アプリをデプロイして動作確認します。

## ApplicationInsights パッケージをインストールします
dotnet add package Microsoft.Azure.WebJobs.Logging.ApplicationInsights

## ローカル環境ではアプリにアクセスしてもエラーになり動作しませんでした
func start

## Azure Functions にアプリをデプロイします 
func azure functionapp publish ${prefix}

## name パラメーターに「 Sato 」をセットして Azure Functions にアプリにアクセスします
curl -s "https://${prefix}.azurewebsites.net/api/test?name=Sato"


アプリのコードに追加した「 Function called 」カスタムイベントと「 evt.Context.User.Id 」の値を Application Insights と Log Analytics で確認します。
リアルタイムにイベント出力されませんが、数分待てば下記のようにイベント出力が確認できます。

ワークスペース ID やプライマリキーを扱わずに Application Insights と Log Analytics にカスタムイベントを送信する事ができました。


Application Insights のカスタムイベントは、下記のようにして確認する事もできます。

まとめ

今回の検証で Azure Functions アプリのログとカスタムイベントを Application Insights 経由で簡単に Log Analytics に送信できる事がわかりました。

要件次第ですが、今回の Azure Functions アプリは Application Insights にだけ情報を送信して、自動的に Log Analytics にもログとカスタムイベントが保管される、という方法が良さそうです。

このブログ記事が誰かの何かの参考になればうれしいです。 最後まで読んで頂き、ありがとうございます。

お問い合わせ

製品・サービスに関するお問い合わせはお気軽にご相談ください。

ピックアップ

セミナー情報
クラウドエンジニアブログ
clouXion
メールマガジン登録