概要 †Azure Monitor の統合アラート エクスペリエンスには、以前は Log Analytics と Application Insights によって管理されていたアラートも含まれるようになった。 目次 †
どのようなログを検知するかを決める。 †Azure Monitor では、設定したクエリに合致するレコードの件数や、数値の平均値などが一定数以上の時に通知を行う事ができる。
上記の前提で、今回は異常チェック用として以下のクエリを使用する。 traces | where (message has_cs "[ERROR] Result:" or message has_cs "http: panic serving") and cloud_RoleName == "関数アプリ名(全て小文字)" and timestamp > ago (1h) ※ Kustoクエリについては Kusto クエリ言語 を参照。 サンプルアプリ †ストレージコンテナ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ポータルで "モニター" を検索し、選択。 アラートから [新しいアラートルール] を選択。 関数アプリに関連付けている Application Insight を選択し、条件名のリンクをクリック。 custom log search で下図のように入力/選択。 [アクショングループの選択] を押下。 リソースグループ、アクショングループ名などを入力。(まだ [確認及び作成] は押さない) [通知]タブに切り替えて、通知の種類、名前を入力。(電子メール/プッシュ通知... を選択する) 通知先のメールアドレスを入力し [OK] アクションタブでは他の関数の起動等を設定できるが、今回は何も指定せずに [確認及び作成]。 メールの件名などを入力し [アラートルールの作成] を押下。 エラー通知の例 †上記で設定したアラートに合致するログが見つかった時には、下図のようなメールが送信されてくる。 メール本文の [View 10 Result(s)] ボタンを押下すると Azure ポータルの Insight のログ検索画面が開き、結果が表示される。 料金 †https://azure.microsoft.com/ja-jp/pricing/details/monitor/ には下表の通り記載されている。(2020/8月現在) アラート †
通知 †
※SMS と音声通話は省略 |