Azure Blobトリガーから起動される関数アプリがコケた時にメール通知を行う方法について記載する。
Azure Functions の異常を検知する では、Azure Monitor を使用してメール通知を行ったが、Azure Monitor ではメールの本文を指定して送信する事ができない為、
ここでは有害キュー(webjobs-blobtrigger-poison)にトリガーを設定して関数アプリからメール送信を行う方法について記載する。
また、関数アプリの言語は従量課金プランが可能な Node.js を使用している。
[補足]
※複数(5回のリトライ回数分)の通知が来ても良いなら ログの出力形式だけ決めておいて Azure Monitor から通知しても良いと思う。
{ "version": "2.0", "extensions": { "queues": { "maxPollingInterval": "00:00:10", "visibilityTimeout" : "00:00:00", "batchSize": 16, "maxDequeueCount": 1, "newBatchThreshold": 8 } }, "extensionBundle": { "id": "Microsoft.Azure.Functions.ExtensionBundle", "version": "[1.*, 2.0.0)" } }
{ "IsEncrypted": false, "Values": { "AzureWebJobsStorage": "UseDevelopmentStorage=true", "SENDGRID_APIKEY": "SendGridのAPIキー", "MAIL_FROM_ADDRESS": "送信元アドレス", "MAIL_TO_ADDRESS": "送信先アドレス" } }
{ "name": "sample_sendgrid_nodejs", "version": "1.0.0", "main": "myfunc.js", "dependencies": { "@sendgrid/mail": "^7.2.6" } }
{ "disabled": false, "bindings": [ { "type": "queueTrigger", "direction": "in", "name": "queueItem", "queueName": "webjobs-blobtrigger-poison" } ] }
module.exports = async function (context, message) { context.log('queue item:', message); // Dequeue1回目の時のみメール通知 if (context.bindingData.dequeueCount === 1) { SENDGRID_APIKEY = process.env.SENDGRID_APIKEY MAIL_FROM_ADDRESS = process.env.MAIL_FROM_ADDRESS MAIL_TO_ADDRESS = process.env.MAIL_TO_ADDRESS const sgMail = require('@sendgrid/mail'); sgMail.setApiKey(SENDGRID_APIKEY); const msg = { to: MAIL_TO_ADDRESS, from: MAIL_FROM_ADDRESS, subject: 'サンプルメール', text: 'Node.js からのメール送信テスト' + message, html: '<strong>Blobトリガーの処理失敗</strong><br />' + '<div style="border:1px solid #333; padding:10px; display: inline-block;">' + message + '</div>', }; await (async () => { try { await sgMail.send(msg); context.log('Mail sended.'); } catch (error) { context.log.error("[MAILSEND ERROR]", error, error.response.body) } })(); } context.log('DequeueCount: ', context.bindingData.dequeueCount); // 成功させるとキューからメッセージが削除されてしまう為、あえて失敗させる。 //context.done(); throw new Error('The message is not deleted because it is only a notification'); };
blobトリガー用の関数アプリに含めてしまう場合(Go)の例
import ( : "github.com/sendgrid/sendgrid-go" "github.com/sendgrid/sendgrid-go/helpers/mail" ) : /** * エラーのメール通知. */ func blobErrorHandler(w http.ResponseWriter, r *http.Request) { var invokeReq InvokeRequest d := json.NewDecoder(r.Body) decodeErr := d.Decode(&invokeReq) if decodeErr != nil { http.Error(w, decodeErr.Error(), http.StatusBadRequest) return } printInfo("queue metadata: %v", invokeReq.Metadata) queueItem := invokeReq.Data["queueItem"].(string) printInfo("queue message: %+v", queueItem) SENDGRID_API_KEY := getEnv("SENDGRID_API_KEY", "") MAIL_FROM_ADDRESS := getEnv("MAIL_FROM_ADDRESS", "") MAIL_TO_ADDRESS := getEnv("MAIL_TO_ADDRESS", "") // 見やすいように過剰エスケープを調整 queueItem = strings.TrimRight(queueItem, "\"") queueItem = strings.TrimLeft(queueItem, "\"") queueItem = strings.Replace(queueItem, "\\n", "\n", -1) queueItem = strings.Replace(queueItem, "\\\"", "\"", -1) queueItem = strings.Replace(queueItem, "\\\\\"", "", -1) printInfo("queue message(Value): %v", queueItem) from := mail.NewEmail("トリガー異常監視", MAIL_FROM_ADDRESS) subject := "Blobトリガーでエラーが発生しています" to := mail.NewEmail(MAIL_TO_ADDRESS, MAIL_TO_ADDRESS) plainTextContent := fmt.Sprintf("有害キューに出力された情報:\n%s", queueItem) htmlContent := fmt.Sprintf("有害キューに出力された情報:<br /><pre style=\"border: 1px solid #333; padding: 10px;\">%s</pre>", queueItem) message := mail.NewSingleEmail(from, subject, to, plainTextContent, htmlContent) client := sendgrid.NewSendClient(SENDGRID_API_KEY) response, err := client.Send(message) if err != nil { printError("[MAILSEND ERROR] %v", err) } else if (response.StatusCode == 202) { printInfo("[MAILSEND SUCCESS]") } else { printError("[MAILSEND ERROR] status: %v, body: %v, header: %v", response.StatusCode, response.Body, response.Headers) } // 正常終了してしまうとキューから消えてしまうので、あえてエラーにしておく //invokeResponse := InvokeResponse{Logs: []string{"Received blob trigger error."} } //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) http.Error(w, "process blobErrorHandler.", http.StatusInternalServerError) } : func main() { httpInvokerPort, exists := os.LookupEnv("FUNCTIONS_HTTPWORKER_PORT") if exists { printInfo("FUNCTIONS_HTTPWORKER_PORT: " + httpInvokerPort) } mux := http.NewServeMux() mux.HandleFunc("/BlobTrigger", blobTriggerHandler) mux.HandleFunc("/RerunAll" , rerunAllHandler) mux.HandleFunc("/ErrorNotice", blobErrorHandler) // 追加 log.Println("Go server Listening...on httpInvokerPort:", httpInvokerPort) log.Fatal(http.ListenAndServe(":"+httpInvokerPort, mux)) }
メール通知イメージ