概要

Azure Monitor の統合アラート エクスペリエンスには、以前は Log Analytics と Application Insights によって管理されていたアラートも含まれるようになった。
ここでは Azure モニター を使用して関数アプリの異常を検知する方法について記載する。

目次

どのようなログを検知するかを決める。

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

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

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

traces
| where (message has_cs "[ERROR] Result:" or message has_cs "http: panic serving")
    and cloud_RoleName == "関数アプリ名(全て小文字)"
    and timestamp > ago (1h)

※ Kustoクエリについては Kusto クエリ言語 を参照。
※ 重大度(severityLevel)等も使用できるが今回は使用していない。

サンプルアプリ

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

{
    "version": "2.0",
    "httpWorker": {
        "description": {
            "defaultExecutablePath": "server.exe"
        }   
    },  
    "extensionBundle": {
        "id": "Microsoft.Azure.Functions.ExtensionBundle",
        "version": "[1.*, 2.0.0)"
    }   
}
{
  "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"
  } ] 
}
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))
}
#!/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
#!/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

リソース作成

./create_resources.sh

関数アプリのデプロイ

./deploy_funcapp.sh

アラートの作成

Azureポータルで "モニター" を検索し、選択。
azure_monitor01.png

アラートから [新しいアラートルール] を選択。
azure_monitor02.png

関数アプリに関連付けている Application Insight を選択し、条件名のリンクをクリック。
azure_monitor03.png

custom log search で下図のように入力/選択。
azure_monitor04.png

[アクショングループの選択] を押下。
azure_monitor05.png

リソースグループ、アクショングループ名などを入力。(まだ [確認及び作成] は押さない)
azure_monitor06.png

[通知]タブに切り替えて、通知の種類、名前を入力。(電子メール/プッシュ通知... を選択する)
azure_monitor07.png

通知先のメールアドレスを入力し [OK]
azure_monitor08.png

アクションタブでは他の関数の起動等を設定できるが、今回は何も指定せずに [確認及び作成]。
azure_monitor09.png

メールの件名などを入力し [アラートルールの作成] を押下。
azure_monitor10.png

エラー通知の例

上記で設定したアラートに合致するログが見つかった時には、下図のようなメールが送信されてくる。
※ クエリに引っかかったログの最初の10件の内容もメール本文に記載されている。

azure_alert_mail_sample1.png

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

azure_alert_mail_clicked1.png

料金

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

アラート

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

通知

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

※SMS と音声通話は省略


添付ファイル: fileazure_alert_mail_clicked1.png 284件 [詳細] fileazure_alert_mail_sample1.png 292件 [詳細] fileazure_monitor10.png 264件 [詳細] fileazure_monitor04.png 296件 [詳細] fileazure_monitor07.png 266件 [詳細] fileazure_monitor06.png 256件 [詳細] fileazure_monitor05.png 278件 [詳細] fileazure_monitor09.png 268件 [詳細] fileazure_monitor08.png 320件 [詳細] fileazure_monitor03.png 276件 [詳細] fileazure_monitor02.png 259件 [詳細] fileazure_monitor01.png 285件 [詳細]

トップ   差分 バックアップ リロード   一覧 単語検索 最終更新   ヘルプ   最終更新のRSS
Last-modified: 2020-08-31 (月) 08:34:52 (1331d)