Azure Functions のエラーを検知してメール通知したいのに、ログアラートのメールでは KQL の operation_Name(関数名)が展開されず困ることがあります。本記事では App Insights/Log Analytics の KQL設計と、Action Group→Azure Automation Runbook でアラート JSON を整形して「関数名入りメール」を送る手順を解説します。
結論:ログアラートの「メール本文」にKQLの列を差し込むのは難しい。だからRunbookで作る
Azure Monitor のログ アラート(Log search alert / Log Alerts V2)は、KQL の検索結果に含まれる列(例:operation_Name / OperationName)を、アラートの標準メール本文へ “そのまま差し込む” 使い方が得意ではありません。特に、設定画面の Advanced options → Custom properties に ${operation_Name} や {operation_Name} を書いても、期待どおりに本文へ展開されないケースが典型です。
一方で、アラート通知は(Common Alert Schema を含む)JSON ペイロードとして外部に渡せます。つまり、「アラート→Runbook起動→RunbookがJSONを解析→自前テンプレでメール送信」に切り替えると、関数名を含むメール本文を自由に作れます。
まず押さえる:Azure Functions の例外は AppExceptions / exceptions に入る
Functions のランタイムエラーや未処理例外は、Application Insights の例外テレメトリとして記録されます。環境が「Application Insights(従来の名前)」で見えているか、「Log Analytics(ワークスペース)」として見えているかで、テーブル名・列名が少し変わるのが落とし穴です。
| 観点 | Application Insights 側(従来) | Log Analytics 側(ワークスペース) | メモ |
|---|---|---|---|
| 例外テーブル | exceptions | AppExceptions | どちらも「例外」を表すが、名前が違う |
| 時刻列 | timestamp | TimeGenerated | アラート用途では時間フィルタが重要 |
| 関数名が入ることが多い列 | operation_Name | OperationName | 同じ概念でも表記が違う |
また、ワークスペース側(AppExceptions)では、OperationName が「アプリケーションの操作名」で、AppRequests の Name と一致することが多い、と明記されています。Functions の場合、ここに関数名が入ることが多いため、メールに関数名を出したいときの第一候補になります。
なぜ「Custom properties に ${operation_Name}」がメールに出ないのか
ここが今回のハマりどころです。Azure Monitor の通知では、Custom properties は “何に渡されるか” が決まっています。公式のトラブルシューティングでは、Custom properties は webhook / Azure Functions / Logic Apps などのペイロードには渡るが、メール/SMS/プッシュ通知には含まれないと説明されています。
つまり、Custom properties は「メール本文のテンプレート差し込み」用途ではなく、Webhook等に渡すための拡張メタデータとして扱うのが正解です。
なお、ログアラートのメール件名については、Common Alert Schema のパスを使って動的値を入れる機能が追加されています(プレビュー)。ただし、これは “件名” の話であり、KQL の各行の列を自由に本文へ埋め込める話とは別です。関数名を確実に入れたいなら、やはり Runbook 側で本文を組み立てるほうが確実です。
実装方針:Logic Apps を使わず「Action Group → Automation Runbook」で完結させる
構成はシンプルです。ログアラート自体は “検知” に徹し、通知の中身の整形は Runbook に寄せます。
Azure Functions
↓(例外が発生)
Application Insights / Log Analytics(AppExceptions)
↓(ログ アラートのKQLが一致)
Azure Monitor Log Alert(Log Alerts V2)
↓(Action Group)
Azure Automation Runbook(Webhook起動)
↓(JSON解析 → メール本文生成)
SMTP 587 / Microsoft Graph / SendGrid などで送信
Runbook へはアラート通知が HTTP POST(Webhook)で渡され、本文に JSON が入ります。Runbook 側は WebhookData.RequestBody を JSON としてパースし、必要な値(関数名、例外メッセージ、発生時刻、ポータルリンク等)を抽出してメールを作ります。
KQL の作り方:Runbookが読みやすい列名に「先に整形」しておく
Runbook でパースする前提なら、KQL 側で列名を分かりやすく固定しておくと運用が楽になります。特におすすめは、関数名を FunctionName のような自分で決めた列名で投影(project)しておくことです。
ワークスペース(AppExceptions)向け KQL 例
AppExceptions
| where TimeGenerated > ago(10m)
| where SeverityLevel >= 3
| order by TimeGenerated desc
| take 1
| project
TimeGenerated,
FunctionName = OperationName,
ExceptionType,
ErrorMessage = OuterMessage,
OperationId,
ProblemId,
AppRoleName,
Details
Application Insights(exceptions)向け KQL 例
exceptions
| where timestamp > ago(10m)
| order by timestamp desc
| take 1
| project
TimeGenerated = timestamp,
FunctionName = operation_Name,
ExceptionType = type,
ErrorMessage = message,
OperationId = operation_Id,
Details = details
ポイントは「Runbook が拾う列名」を固定することです。FunctionName という列が必ず出るようにしておけば、Runbook側は $row.FunctionName を読むだけで済みます。
アラート用KQLで意識したい設計ポイント
| 観点 | おすすめ | 理由 |
|---|---|---|
| 時間範囲 | where TimeGenerated > ago(5m〜15m) | アラート評価のたびに古い例外を拾うのを防ぐ |
| 返す列 | 必要最小限に絞る | 通知ペイロードが大きいと検索結果が省略されやすい |
| 列名 | FunctionName などに別名を付ける | Runbook での実装が安定する |
| 重複通知 | 運用に応じて “集計” も検討 | 短時間に同種例外が連発するとメール嵐になる |
ログ アラート ルール設定のコツ
ログ アラートは「KQLで条件に一致したら発火」の仕組みです。基本は以下の考えでOKです。
- Measure は Table rows(行数)
- Operator は Greater than、Threshold は 0(=1行でも返ったら発火)
- 評価頻度(Evaluation frequency)とクエリ時間範囲(Window size)は、通知遅延と重複を見ながら調整
ログ アラートは Common Alert Schema のペイロードで通知される前提になっています。Runbook で扱いやすくなるので、基本はこの前提で進めるのが安全です。
Action Group で Runbook を呼ぶときの注意点
Action Group から Runbook を呼び出す場合、実体は Runbook の Webhook エンドポイントへ HTTP POST する動きになります。つまり、Runbook は “Webhook から起動される前提” の入力パラメータ(WebhookData)で受け取る形が基本です。
また、Automation Account 側を Private Link 前提にしている場合、アラートから Webhook を叩けない(Public access が無効だとトリガーできない)という注意点があります。閉域化を進めている環境では、ここで詰まりやすいので先に確認してください。
Runbook 実装例:関数名(FunctionName)を拾ってメール本文を作る
ここからが本題です。以下は “ログアラート → Runbook起動” を前提に、
- アラート JSON を解析
- 検索結果がペイロードに入っていればそこから抽出
- 入っていなければ
linkToSearchResultsAPI(Log Analytics API)で結果を取りに行く - 関数名入りのHTMLメールを組み立て
までをまとめた PowerShell(Azure Automation 用)のサンプルです。
param(
[Parameter(Mandatory=$false)]
[object] $WebhookData
)
Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"
function Convert-TablesToObjects {
param([Parameter(Mandatory=$true)] $Tables)
if (-not $Tables -or $Tables.Count -eq 0) { return @() }
$t = $Tables[0]
if (-not $t.columns -or -not $t.rows) { return @() }
$colNames = @($t.columns | ForEach-Object { $_.name })
$out = New-Object System.Collections.Generic.List[object]
foreach ($r in $t.rows) {
$h = [ordered]@{}
for ($i=0; $i -lt $colNames.Count; $i++) {
$h[$colNames[$i]] = $r[$i]
}
$out.Add([pscustomobject]$h)
}
return ,$out
}
function Get-LogAnalyticsAccessToken {
# Az.Accounts が入っている前提。必要なら Automation Account にモジュールをインポート
Disable-AzContextAutosave -Scope Process | Out-Null
Connect-AzAccount -Identity | Out-Null
$tok = (Get-AzAccessToken -ResourceUrl "https://api.loganalytics.io").Token
if (-not $tok) { throw "Failed to get token for Log Analytics API." }
return $tok
}
function Invoke-LogAnalyticsQueryFromLink {
param(
[Parameter(Mandatory=$true)][string] $LinkToSearchResultsApi
)
$token = Get-LogAnalyticsAccessToken
$headers = @{ Authorization = "Bearer $token" }
# linkToSearchResultsAPI は GET で叩ける形(query と timespan が埋め込まれている想定)
$resp = Invoke-RestMethod -Method Get -Uri $LinkToSearchResultsApi -Headers $headers
if (-not $resp.tables) { return @() }
return (Convert-TablesToObjects -Tables $resp.tables)
}
if (-not $WebhookData) {
throw "This runbook must be triggered via Azure Monitor alert action group (webhook)."
}
$payload = $WebhookData.RequestBody | ConvertFrom-Json
# 共通で使う変数
$alertRuleName = $null
$severity = $null
$firedAt = $null
$linkUi = $null
$linkApi = $null
$rows = @()
# 1) Common Alert Schema か判定
if ($payload.schemaId -eq "azureMonitorCommonAlertSchema") {
$ess = $payload.data.essentials
$ctx = $payload.data.alertContext
$alertRuleName = $ess.alertRule
$severity = $ess.severity
$firedAt = $ess.firedDateTime
# Log Alerts V2 の検索条件(先頭1件だけ使う想定)
$criteria = $ctx.condition.allOf[0]
$linkUi = $criteria.linkToSearchResultsUI
$linkApi = $criteria.linkToSearchResultsAPI
# ペイロードに検索結果が入っているケース(環境や設定で異なる)
if ($criteria.searchResults -and $criteria.searchResults.tables) {
$rows = Convert-TablesToObjects -Tables $criteria.searchResults.tables
}
}
else {
# 古いスキーマ(SearchResult が root だったり data 配下だったりする)も拾う
if ($payload.AlertRuleName) { $alertRuleName = $payload.AlertRuleName }
if ($payload.Severity) { $severity = $payload.Severity }
if ($payload.LinkToSearchResults) { $linkUi = $payload.LinkToSearchResults }
if ($payload.LinkToSearchResultsAPI) { $linkApi = $payload.LinkToSearchResultsAPI }
if ($payload.SearchResult -and $payload.SearchResult.tables) {
$rows = Convert-TablesToObjects -Tables $payload.SearchResult.tables
}
elseif ($payload.data -and $payload.data.SearchResult -and $payload.data.SearchResult.tables) {
$rows = Convert-TablesToObjects -Tables $payload.data.SearchResult.tables
}
}
# 2) rows が空なら API から取りに行く(リンクがある場合)
if ((-not $rows) -or $rows.Count -eq 0) {
if ($linkApi) {
$rows = Invoke-LogAnalyticsQueryFromLink -LinkToSearchResultsApi $linkApi
}
}
# 3) ここで「Cannot index into a null array」を潰す:0件を必ず想定
$first = $null
if ($rows -and $rows.Count -gt 0) { $first = $rows[0] }
# 4) 取りたい値(KQLで FunctionName を作っておくと安定)
$functionName = if ($first -and $first.PSObject.Properties.Name -contains "FunctionName") { [string]$first.FunctionName } else { "(不明)" }
$errorMessage = if ($first -and $first.PSObject.Properties.Name -contains "ErrorMessage") { [string]$first.ErrorMessage } else { "(メッセージなし)" }
$exceptionType = if ($first -and $first.PSObject.Properties.Name -contains "ExceptionType") { [string]$first.ExceptionType } else { "" }
$timeGenerated = if ($first -and $first.PSObject.Properties.Name -contains "TimeGenerated") { [string]$first.TimeGenerated } else { "" }
$operationId = if ($first -and $first.PSObject.Properties.Name -contains "OperationId") { [string]$first.OperationId } else { "" }
# 5) HTMLメール本文(WordPress向けに最小限のタグで)
$subject = "[Azure Functions] 例外検知: $functionName"
$body = @"
<html>
<body>
<p>Azure Functions で例外が検知されました。</p>
<table border="1" cellpadding="6" cellspacing="0">
<tr><th align="left">アラート ルール</th><td>$alertRuleName</td></tr>
<tr><th align="left">Severity</th><td>$severity</td></tr>
<tr><th align="left">関数名</th><td><b>$functionName</b></td></tr>
<tr><th align="left">例外型</th><td>$exceptionType</td></tr>
<tr><th align="left">メッセージ</th><td>$errorMessage</td></tr>
<tr><th align="left">発生時刻</th><td>$timeGenerated</td></tr>
<tr><th align="left">OperationId</th><td>$operationId</td></tr>
</table>
<p>詳細確認(ポータル):<br/>$linkUi</p>
</body>
</html>
"@
# 6) 送信(ここでは SMTP 587 の例。25番は避ける)
# Automation アセットに PSCredential を登録しておき、名前で取得する想定
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
$cred = Get-AutomationPSCredential -Name "SmtpCredential"
Send-MailMessage `
-To "[email protected]" `
-From "[email protected]" `
-Subject $subject `
-Body $body `
-BodyAsHtml `
-SmtpServer "smtp.office365.com" `
-Port 587 `
-UseSsl `
-Credential $cred
上のサンプルは、実務でハマりやすいポイント(スキーマ差、検索結果なし、配列null)をまとめて避ける作りにしています。特に重要なのは、「検索結果が 0 件のとき」でも落ちないことです。$rows[0] を読みに行く前に、必ず件数チェックを入れてください。
「Cannot index into a null array」が出る典型パターンと対策
Runbook でこのエラーが出るときは、ほぼ次のどれかです。
| 原因 | 起きること | 対策 |
|---|---|---|
| 評価タイミングで KQL が 0 件 | rows が空 → rows[0] で落ちる | if ($rows -and $rows.Count -gt 0) のガードを必ず入れる |
| 通知ペイロードが大きく検索結果が省略 | ペイロードに SearchResult / searchResults が無い | linkToSearchResultsAPI から API で再取得する |
| テーブル名・列名の差を吸収していない | OperationName と operation_Name の違いで空扱い | KQLで FunctionName を作って列名を固定する |
SMTP のタイムアウト(port 25)が起きる理由と、実務での回避策
Runbook から社内SMTP(TCP 25)へ送ろうとしてタイムアウトするのは、環境依存の “あるある” です。Azure ではサブスクリプション種別など条件により、アウトバウンドの SMTP 25 番がブロックされることがあり、公式ドキュメントでも「多くのサブスクリプションタイプで TCP 25 をブロックする」旨が説明されています。
そのため、メール送信は「25番前提」の設計を避けるのが現実的です。Runbook で選べる代替案を、実務目線で比較します。
| 方式 | おすすめ度 | メリット | 注意点 |
|---|---|---|---|
| SMTP 25 | 低 | 既存SMTPを使える | Azure側の制約で詰まりやすい(ブロック/到達性) |
| SMTP 587(TLS) | 中 | 25番より成功率が高い。既存のSMTP運用を維持しやすい | 認証方式(SMTP AUTH)やテナント制御に左右される |
| Microsoft Graph(推奨) | 高 | SMTPに依存しない。マネージドID等で安全に実装しやすい | Mail.Send など権限設計が必要 |
| SendGrid 等のメールAPI | 高 | 到達性と運用が安定。Automation公式でも例がある | APIキー管理(Key Vault推奨) |
| Hybrid Runbook Worker | 社内SMTP必須なら本命 | Runbookを自社VNet内(VM)で動かせる | Worker側の運用(パッチ、監視)が増える |
代替案:SendGrid / Graph で送ると運用が楽になる
SendGrid(Key Vault でキー管理)
Azure Automation から SendGrid を使ってメールを送る手順は公式に整理されています。Key Vault に SendGrid の API キーを入れ、Runbook はマネージドIDで Key Vault からシークレットを取得して送信する流れです。SMTP の到達性問題から解放されるのが大きなメリットです。
Microsoft Graph(マネージドIDでトークン取得→sendMail)
Microsoft Graph を使う場合は、Runbook(マネージドID)で Graph のアクセストークンを取得し、/users/{sender}/sendMail を叩いて送信します。Q&A でも、マネージドIDでトークンを取って Graph で送信する例が紹介されています(権限として Mail.Send などが必要)。
「通知メールは Microsoft 365 の共有メールボックスから送りたい」「SMTP AUTH を使いたくない(禁止されている)」といった組織ポリシーがある場合、Graph 方式がハマりやすいです。
実務で“ちゃんと使える通知”にするためのチェックリスト
関数名入りのメールが送れるようになっても、運用で詰まるポイントは残ります。最後に、アラート運用を安定させるためのチェック項目をまとめます。
| チェック項目 | 狙い | 実装のヒント |
|---|---|---|
| KQLに時間フィルタがある | 古い例外で毎回発火しない | ago(5m)〜ago(15m) を基本に調整 |
| KQLの列名が固定されている | Runbookが壊れにくい | FunctionName、ErrorMessage などに project で揃える |
| 検索結果0件でも落ちない | Runbookの例外で二次障害にしない | 配列の null/件数チェックを徹底 |
| 検索結果が省略されたときの保険がある | ペイロードサイズに左右されない | linkToSearchResultsAPI から再取得する分岐 |
| メール送信経路がAzure制約に強い | 送れない通知を作らない | 25番を避け、587/Graph/API を優先 |
まとめ
Azure Functions の失敗を “関数名入りメール” で通知したい場合、ログアラートの標準メールに KQL 列を直接差し込もうとしても行き詰まりがちです。Custom properties はメール通知に含まれないため、Runbook 側でアラート JSON を解析してメールを自作するのが現実解になります。
例外検出は exceptions / AppExceptions を基本にし、KQL では FunctionName のような列を明示的に作っておくと Runbook 実装が安定します。Runbook は検索結果が空のケースやペイロード都合で結果が省略されるケースを想定し、API から再取得できるようにしておくと堅牢です。
メール送信は SMTP 25 番に依存しない設計(587、Graph、SendGrid 等)へ寄せるほど運用が楽になります。どうしても社内SMTPなど閉域リソースが必須なら、Hybrid Runbook Worker を含めた「実行場所の見直し」も検討してください。

コメント