#mynavi(Azureメモ)
#setlinebreak(on);

#html(){{
<style>
.images img { border: 1px solid #333; margin-right: 20px;}
.images div { vertical-align: top; }
</style>
}}

* 概要 [#l83f8e0c]
#html(<div class="pl10">)
Azure Monitor の統合アラート エクスペリエンスには、以前は Log Analytics と Application Insights によって管理されていたアラートも含まれるようになった。
ここでは Azure モニター を使用して関数アプリの異常を検知する方法について記載する。
#html(</div>)

* 目次 [#q34c50fa]
#contents
- 関連
-- [[Azureメモ]]
-- [[Azure Functions のログを参照する]]
- 参考
-- [[Azure Monitor の概要>https://docs.microsoft.com/ja-jp/azure/azure-monitor/overview]]
-- [[Azure App Service のアプリの監視>https://docs.microsoft.com/ja-jp/azure/app-service/web-sites-monitor]]
-- [[Azure での Web アプリケーションの監視>https://docs.microsoft.com/ja-jp/azure/architecture/reference-architectures/app-service-web-app/app-monitoring]]
-- [[Microsoft Azure のアラートの概要>https://docs.microsoft.com/ja-jp/azure/azure-monitor/platform/alerts-overview]]
-- [[Azure Functions を監視する>https://docs.microsoft.com/ja-jp/azure/azure-functions/functions-monitoring?tabs=cmd]]
-- [[Application Insights の使用量とコストを管理する>https://docs.microsoft.com/ja-jp/azure/azure-monitor/app/pricing]]
-- [[Azure Monitor でログクエリの使用を開始する>https://docs.microsoft.com/ja-jp/azure/azure-monitor/log-query/get-started-queries]]
-- [[Kusto の概要>https://docs.microsoft.com/ja-jp/azure/data-explorer/kusto/concepts/]]
-- [[Kusto クエリ言語>https://docs.microsoft.com/ja-jp/azure/data-explorer/kusto/query/]]

* どのようなログを検知するかを決める。 [#m56ceb07]
#html(<div class="pl10">)

Azure Monitor では、設定したクエリに合致するレコードの件数や、数値の平均値などが一定数以上の時に通知を行う事ができる。
今回は以下の前提で以降の手順を記載する。

- 関数アプリは Go で作成されているものとする。(カスタムハンドラーを使用)
- 関数アプリは処理の最後に処理結果を以下の形式で出力する。
| 正常/異常 | 出力されるログ |h
| 正常時 | [INFO] Result: Success ... |
| 異常時時 | [ERROR] Result: Failure ... |
- 異常チェックは1時間に一回行うものとする。

上記の前提で、今回は異常チェック用として以下のクエリを使用する。

#myterm2(){{
traces
| where (message has_cs "[ERROR] Result:" or message has_cs "http: panic serving")
    and cloud_RoleName == "関数アプリ名(全て小文字)"
    and timestamp > ago (1h)
}}
※ Kustoクエリについては [[Kusto クエリ言語>https://docs.microsoft.com/ja-jp/azure/data-explorer/kusto/query/]] を参照。 
※ 重大度(severityLevel)等も使用できるが今回は使用していない。

#html(</div>)

* サンプルアプリ [#lf2a9e7c]
#html(<div class="pl10">)

ストレージコンテナ1にアップロードされたCSVファイルをストレージコンテナ2にそのまま出力するシンプルな関数アプリ。

#html(){{
<div id="tabs1">
  <ul>
    <li><a href="#tabs1-1">functions/host.json</a></li>
    <li><a href="#tabs1-2">functions/MyBlobTrigger/function.json</a></li>
    <li><a href="#tabs1-3">functions/server.go</a></li>
    <li><a href="#tabs1-4">create_resources.sh</a></li>
    <li><a href="#tabs1-5">deploy_funcapp.sh</a></li>
  </ul>
}}

// START tabs1-1
#html(<div id="tabs1-1">)

#mycode2(){{
{
    "version": "2.0",
    "httpWorker": {
        "description": {
            "defaultExecutablePath": "server.exe"
        }   
    },  
    "extensionBundle": {
        "id": "Microsoft.Azure.Functions.ExtensionBundle",
        "version": "[1.*, 2.0.0)"
    }   
}
}}

#html(</div>)
// END tabs1-1

// START tabs1-2
#html(<div id="tabs1-2">)

#mycode2(){{
{
  "bindings" : [ { 
    "type" : "blobTrigger",
    "direction" : "in",
    "name" : "blobData",
    "path" : "ストレージコンテナ名1/{name}",
    "dataType" : "binary",
    "connection" : "AzureWebJobsStorage"
  },  
  {
    "type" : "blob",
    "direction" : "out",
    "name" : "$return",
    "path" : "ストレージコンテナ名2/{name}",
    "dataType" : "binary",
    "connection" : "AzureWebJobsStorage"
  } ] 
}
}}

#html(</div>)
// END tabs1-2

// START tabs1-3
#html(<div id="tabs1-3">)

#mycode2(){{
package main

import (
    "encoding/base64"
    "encoding/json"
    "fmt"
    "log"
    "net/http"
    "os"
    "strings"
)

type ReturnValue struct {
    Data string
}
type InvokeResponse struct {
    Outputs     map[string]interface{}
    Logs        []string
    ReturnValue interface{}
}

type InvokeRequest struct {
    Data     map[string]interface{}
    Metadata map[string]interface{}
}

func printDebug(format string, params ...interface{}){
    log.SetOutput(os.Stdout)
    msg := fmt.Sprintf(format, params...)
    log.Printf("[DEBUG] %s\n", msg)
}

func printInfo(format string, params ...interface{}){
    log.SetOutput(os.Stdout)
    msg := fmt.Sprintf(format, params...)
    log.Printf("[INFO] %s\n", msg)
}

func printError(format string, params ...interface{}){
    log.SetOutput(os.Stderr)
    msg := fmt.Sprintf(format, params...)
    log.Printf("[ERROR] %s\n", msg)
    log.SetOutput(os.Stdout)
}

func init(){
    log.SetOutput(os.Stdout)
    log.SetFlags(0)
}

func blobTriggerHandler(w http.ResponseWriter, r *http.Request) {

    printInfo("START blobTriggerHandler")

    fileUri := ""

    defer func(){
        err := recover()
        if fileUri == "" {
            fileUri = "unknown"
        }
        if err != nil {
            printError("Result: Failure %s", fileUri)
            panic("blobTriggerHandler Error!\n");
        } else {
            printInfo("Result: Success %s", fileUri)
        }
    }()

    logs := make([]string, 0)

    //------------------------------------------------
    // データ取得
    //------------------------------------------------
    var invokeReq InvokeRequest
    d := json.NewDecoder(r.Body)
    decodeErr := d.Decode(&invokeReq)
    if decodeErr != nil {
        http.Error(w, decodeErr.Error(), http.StatusBadRequest)
        return
    }
    fileUri  = invokeReq.Metadata["Uri"].(string)
    filename := invokeReq.Metadata["name"].(string)
    sysInfo  := fmt.Sprintf("%v", invokeReq.Metadata["sys"])
    fileData, _ := base64.StdEncoding.DecodeString(invokeReq.Data["blobData"].(string))

    printDebug("filename  : %s", filename)
    printDebug("fileUri   : %s", fileUri)
    printDebug("sysinfo   : %s", sysInfo)

    //------------------------------------------------
    // CSV文字列をパースする
    //------------------------------------------------
    rows := parseCsv(string(fileData), fileUri)
    printDebug("csv data: %v", rows)

    //------------------------------------------------
    // レスポンス生成
    //------------------------------------------------
    returnValue := fileData
    invokeResponse := InvokeResponse{Logs: logs, ReturnValue: string(returnValue)}

    js, err := json.Marshal(invokeResponse)
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }

    w.Header().Set("Content-Type", "application/json")
    w.Write(js)

    printInfo("END blobTriggerHandler")
}

/**
 * CSV文字列のパース.
 */
func parseCsv(csvText string, fileUri string) ([]map[string]string) {

    procIndex := -1
    defer func(){
        err := recover()
        if err != nil {
            printError("error: file: %s, line: %d, %v", fileUri, procIndex, err)
            panic("parseCsv Error!\n");
        }
    }()

    lines := strings.Split(csvText, "\n")
    var columns []string
    rows := make([]map[string]string, 0)
    for i, line := range lines {
        procIndex = i
        if i == 0 {
            columns = strings.Split(line, ",")
        } else {
            values := strings.Split(line, ",")
            row := make(map[string]string, len(values))
            for j, val := range values {
                // ヘッダの列数より多い時はわざとコケるようにしておく
                colname := columns[j]
                row[colname] = val
            }
            rows = append(rows, row)
        }
    }

    return rows
}

/**
 * メイン.
 */
func main() {
    httpInvokerPort, exists := os.LookupEnv("FUNCTIONS_HTTPWORKER_PORT")
    if exists {
        log.Println("FUNCTIONS_HTTPWORKER_PORT: " + httpInvokerPort)
    }
    mux := http.NewServeMux()
    mux.HandleFunc("/MyBlobTrigger", blobTriggerHandler)
    log.Println("Go server Listening...on httpInvokerPort:", httpInvokerPort)
    log.Fatal(http.ListenAndServe(":"+httpInvokerPort, mux))
}
}}

#html(</div>)
// END tabs1-3

// START tabs1-4
#html(<div id="tabs1-4">)

#mycode2(){{
#!/bin/bash

# 全てのリソース名に付与する接頭文字 (Storageアカウント名などは世界でユニークな必要があるので他ユーザと被らないような名前を付ける)
PREFIX=XXXXXXX
subscriptionId=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

region=japanwest
insightsRegion=japaneast # 2020/7時点では Appplication Insights で西日本(japanwest)は使用できない
resourceGroup=${PREFIX}ResourceGroup
storageAccountName=${PREFIX}straccount
storageSku=Standard_LRS
storageContainerIn=${PREFIX}-strcontainer-i
storageContainerOut=${PREFIX}-strcontainer-o
funcAppName=${PREFIX}FuncApp
funcVersion=2
funcPlanName=${PREFIX}premiumplan
funcPlanSku=EP1
insightsName=${PREFIX}Insights
insightsDays=30

# リソースグループの作成
echo az group create
az group create \
  --name $resourceGroup \
  --location $region

# ストレージアカウントの作成
echo az storage account create
az storage account create \
  --name $storageAccountName \
  --location $region \
  --resource-group $resourceGroup \
  --sku $storageSku

# Storageコンテナの作成
echo "az storage container create(in)"
az storage container create \
  --name $storageContainerIn \
  --resource-group $resourceGroup \
  --account-name $storageAccountName
echo "az storage container create(out)"
az storage container create \
  --name $storageContainerOut \
  --resource-group $resourceGroup \
  --account-name $storageAccountName

# Application Insights 拡張が利用できない場合は追加インストール
x=`az monitor app-insights --help 2>&1`
if [ "$?" != "0" ]; then
  az extension add -n application-insights
fi

# Application Insights コンポーネント作成
if [ "$subscriptionId" != "" ]; then
  echo az monitor app-insights component create
  az monitor app-insights component create \
      --app $insightsName \
      --location $insightsRegion \
      --resource-group $resourceGroup \
      --query-access Enabled \
      --retention-time $insightsDays \
      --subscription $subscriptionId
fi

# 関数アプリのプラン作成(プレミアムプラン)
echo az functionapp plan create
az functionapp plan create \
  --name $funcPlanName \
  --resource-group $resourceGroup \
  --location $region \
  --sku $funcPlanSku

# 関数アプリの作成
echo az functionapp create
az functionapp create \
  --name $funcAppName \
  --storage-account $storageAccountName \
  --resource-group $resourceGroup \
  --functions-version $funcVersion \
  --plan $funcPlanName \
  --app-insights $insightsName
}}

#html(</div>)
// END tabs1-4

// START tabs1-5
#html(<div id="tabs1-5">)

#mycode2(){{
#!/bin/bash

PREFIX=XXXXXXX
resourceGroup=${PREFIX}ResourceGroup
funcAppName=${PREFIX}FuncApp

rm -rf functions.zip

exefile=`cat functions/host.json | grep defaultExecutablePath | awk '{print $2}' | sed 's/"//g'`

cd functions
rm -rf $exefile
GOOS=windows GOARCH=amd64 go build -o $exefile

zip -r ../functions.zip *

cd ../

az functionapp deployment source config-zip -g $resourceGroup -n $funcAppName --src functions.zip

rm -rf functions.zip
}}

#html(</div>)
// END tabs1-5

#html(</div>)
// END tabs1

#html(<script>$(function() { $("#tabs1").tabs(); });</script>)

#html(<div class="pl10">)

リソース作成
#myterm2(){{
./create_resources.sh
}}

関数アプリのデプロイ
#myterm2(){{
./deploy_funcapp.sh
}}

#html(</div>)


#html(</div>)


* アラートの作成 [#ua7becfb]
#html(<div class="pl10">)

#html(<div class="images"><div class="ib">)
Azureポータルで "モニター" を検索し、選択。
&ref(azure_monitor01.png,nolink);
#html(</div><div class="ib">)
アラートから [新しいアラートルール] を選択。
&ref(azure_monitor02.png,nolink);
#html(</div><div class="ib">)
関数アプリに関連付けている Application Insight を選択し、条件名のリンクをクリック。
&ref(azure_monitor03.png,nolink);
#html(</div><div class="ib">)
custom log search で下図のように入力/選択。
&ref(azure_monitor04.png,nolink);
#html(</div><div class="ib">)
[アクショングループの選択] を押下。
&ref(azure_monitor05.png,nolink);
#html(</div><div class="ib">)
リソースグループ、アクショングループ名などを入力。(まだ [確認及び作成] は押さない)
&ref(azure_monitor06.png,nolink);
#html(</div><div class="ib">)
[通知]タブに切り替えて、通知の種類、名前を入力。(電子メール/プッシュ通知... を選択する)
&ref(azure_monitor07.png,nolink);
#html(</div><div class="ib">)
通知先のメールアドレスを入力し [OK]
&ref(azure_monitor08.png,nolink);
#html(</div><div class="ib">)
アクションタブでは他の関数の起動等を設定できるが、今回は何も指定せずに [確認及び作成]。
&ref(azure_monitor09.png,nolink);
#html(</div><div class="ib">)
メールの件名などを入力し [アラートルールの作成] を押下。
&ref(azure_monitor10.png,nolink);
#html(</div></div>)

#html(</div>)

* エラー通知の例 [#dce7fbf2]
#html(<div class="pl10">)

上記で設定したアラートに合致するログが見つかった時には、下図のようなメールが送信されてくる。
※ クエリに引っかかったログの最初の10件の内容もメール本文に記載されている。
#html(<div class="images">)
&ref(azure_alert_mail_sample1.png,nolink);
#html(</div>)

メール本文の [View 10 Result(s)] ボタンを押下すると Azure ポータルの Insight のログ検索画面が開き、結果が表示される。

#html(<div class="images">)
&ref(azure_alert_mail_clicked1.png,nolink);
#html(</div>)

#html(</div>)

* 料金 [#f18b5083]
#html(<div class="pl10">)

https://azure.microsoft.com/ja-jp/pricing/details/monitor/ には下表の通り記載されている。(2020/8月現在)

** アラート [#n514d09c]
| アラート シグナル | 含まれている無料ユニット | 料金 |h
| メトリック | 監視対象メトリック時系列 10 個 (1 か月あたり) | 監視対象メトリック時系列 1 つにつき &yen;11.200 (1 か月あたり) |
| ログ        | なし                                                                    |15 分以上の間隔: 監視対象ログ 1 つにつき &yen;56 (1 か月あたり)&br;10 分間隔: 監視対象ログ 1 つにつき &yen;112 (1 か月あたり)&br;5 分間隔: 監視対象ログ 1 つにつき &yen;168 (1 か月あたり) |
| アクティビティ ログ | サブスクリプションあたり 100 ルールが上限 | 無料 |
| 動的しきい値 | なし | 動的しきい値あたりの &yen;11.200/月 |

** 通知 [#wb723a2c]

| 機能 | 含まれている無料ユニット | 料金 |h
| ITSM コネクタの作成イベントまたは更新イベント | 1 か月あたりイベント 1,000 件 | &yen;560/1,000 イベント |
| メール | 1 か月あたりメール 1,000 通 | メール 100,000 通につき &yen;224 |
| (Azure Mobile Apps への) プッシュ通知 | 1 か月あたり通知 1,000 件	| 通知 100,000 回につき &yen;224 |
| Web hook をセキュリティで保護する | セキュリティ保護された 1 つの Web hook | セキュリティ保護された &yen;672/1,000,000 の Web hook |
| webhook | 1 か月あたりの webhook 100,000 件 | webhook 1,000,000 件につき &yen;67.20 |
※SMS と音声通話は省略

#html(</div>)



//* クエリサンプル [#l05d37f4]
//traces | where operation_Name=="BlobTrigger2" and message has_cs "test"
//traces | operation_Name=="MyBlobTrigger2" and message has_cs "test" and timestamp > ago(1h)


トップ   差分 バックアップ リロード   一覧 単語検索 最終更新   ヘルプ   最終更新のRSS