Azure Functions 失敗アラートで関数名(OperationName)入りメールを送る方法|KQL・Log Analytics・Automation Runbook

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 側(ワークスペース)メモ
例外テーブルexceptionsAppExceptionsどちらも「例外」を表すが、名前が違う
時刻列timestampTimeGeneratedアラート用途では時間フィルタが重要
関数名が入ることが多い列operation_NameOperationName同じ概念でも表記が違う

また、ワークスペース側(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 を含めた「実行場所の見直し」も検討してください。

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次