Azure MonitorのアラートをMicrosoft Teamsに通知する方法

はじめに

この記事ではAzure MonitorのアラートをMicrosoft Teamsに通知する方法を紹介します。

Azure Monitorとは

Azure Monitorは、Azureのサービスの正常性やアプリケーションのログなどを監視することができるサービスです。
以下に、Azure Monitorの機能を紹介します。

  • アプリケーション監視 (Application Insights): アプリケーションのパフォーマンス、可用性、ログなどをリアルタイムで監視します。
  • インフラストラクチャ監視 (VM Insights, Container Insights): 仮想マシンやコンテナなどのインフラストラクチャのパフォーマンスと状態を監視します。
  • ネットワーク監視: ネットワークパフォーマンス、接続状況、トラフィック分析を提供します。
  • ログ分析 (Log Analytics): ログデータを収集、検索、分析するためのツールを提供します。

Azure Monitorのアラートと通知

Azure Monitorでは、アラートルールを設定することで、アラートルールの条件に一致する問題が発生した場合にアラートを通知することができます。

アラートルールの設定後、アクショングループという通知方法の設定すると、指定した通知方法で通知されます。
通知方法の種類として、Eメール、SMS、Webhook、Azure Automation Runbook、Azure Functions、Azure Logic Appsなどがあります。

共通アラートスキーマ

Azure Monitorでは共通アラート スキーマ(Common Alert Schema)という、Azureでのアラート通知の標準化されたフォーマットを使用することができます。

このスキーマを使用することで、Application InsightsやLog Analytics、コストアラートなど異なるサービスからのアラートを共通のフォーマットで受け取ることができます。
異なるソースからのアラートを効率的に処理することを目的としています。

共通アラート スキーマには以下のような情報が含まれます。

  • essentials: 基本情報

    • alertId: ユニークなアラート識別子。
    • firedDateTime: アラートが生成された時刻。
    • monitoringService: アラートを検出したAzure Monitorのサービス。
    • description: アラートの説明。
  • alertContext: アラートに関連する詳細な情報やメトリクス。

    • アラートの内容や原因など情報。

コストアラートの共通アラートスキーマ

コストアラートは、Azureのコストが設定した予算額を超過した場合に通知するアラートです。
コストアラートの共通アラートスキーマは以下のようになります。

{
    "schemaId": "azureMonitorCommonAlertSchema",
    "data": {
      "essentials": {
        "monitoringService": "CostAlerts",
        "firedDateTime": "2023-12-05T12:02:54.657Z",
        "description": "Your spend for budget Test_actual_cost_budget is now $11,111.00 exceeding your specified threshold $25.00.",
        "essentialsVersion": "1.0",
        "alertContextVersion": "1.0",
        "alertId": "/subscriptions/11111111-1111-1111-1111-111111111111/providers/Microsoft.CostManagement/alerts/Test_Alert",
        "alertRule": null,
        "severity": null,
        "signalType": null,
        "monitorCondition": null,
        "alertTargetIDs": null,
        "configurationItems": [
          "budgets"
        ],
        "originAlertId": null
      },
      "alertContext": {
        "AlertCategory": "budgets",
        "AlertData": {
          "Scope": "/subscriptions/11111111-1111-1111-1111-111111111111/",
          "ThresholdType": "Actual",
          "BudgetType": "Cost",
          "BudgetThreshold": "$50.00",
          "NotificationThresholdAmount": "$25.00",
          "BudgetName": "Test_actual_cost_budget",
          "BudgetId": "/subscriptions/11111111-1111-1111-1111-111111111111/providers/Microsoft.Consumption/budgets/Test_actual_cost_budget",
          "BudgetStartDate": "2022-11-01",
          "BudgetCreator": "test@sample.test",
          "Unit": "USD",
          "SpentAmount": "$11,111.00"
        }
      }
    }
  }

Log Alerts V2の共通アラートスキーマ

Azure MonitorのLog Alerts V2は、Azure Monitor Log Analyticsのデータに基づいてアラートを作成し管理するための強化されたアラートシステムです。

Log Alerts V2は、Log Analyticsのクエリ機能を使用してログデータを定期的に評価し、特定の条件が満たされた場合にアラートを生成します。

また、Application Insightでアプリケーションログのアラートを設定した際も、Log Alerts V2のスキーマが使用されます。
Application Insightsは、アプリケーションのパフォーマンスや可用性を監視するサービスです。

JSONの項目のうち、linkToSearchResultsAPIとlinkToFilteredSearchResultsAPIのURLを実行すると、Application Insightsで対象のクエリの実行結果の表示が可能です。

{
  "schemaId": "azureMonitorCommonAlertSchema",
  "data": {
    "essentials": {
      "alertId": "/subscriptions/11111111-1111-1111-1111-111111111111/providers/Microsoft.AlertsManagement/alerts/11111111-1111-1111-1111-111111111111",
      "alertRule": "ar-test",
      "severity": "Sev1",
      "signalType": "Log",
      "monitorCondition": "Fired",
      "monitoringService": "Log Alerts V2",
      "alertTargetIDs": [
        "/subscriptions/11111111-1111-1111-1111-111111111111/resourcegroups/rg-test-dev-001/providers/microsoft.insights/components/func-test-dev-eastus-001"
      ],
      "configurationItems": [
        "/subscriptions/11111111-1111-1111-1111-111111111111/resourceGroups/rg-test-dev-001/providers/microsoft.insights/components/func-test-dev-eastus-001"
      ],
      "originAlertId": "11111111-1111-1111-1111-111111111111",
      "firedDateTime": "2023-12-20T01:43:16.4557405Z",
      "description": "Testアラート",
      "essentialsVersion": "1.0",
      "alertContextVersion": "1.0"
    },
    "alertContext": {
      "properties": {
        "includeSearchResults": "True"
      },
      "conditionType": "LogQueryCriteria",
      "condition": {
        "windowSize": "PT5M",
        "allOf": [
          {
            "searchQuery": "traces \r\n| where severityLevel > 2\r\n| project timestamp, message, severityLevel, operation_Id, appId, appName\r\n",
            "metricMeasureColumn": null,
            "targetResourceTypes": "['microsoft.insights/components']",
            "operator": "GreaterThanOrEqual",
            "threshold": "1",
            "timeAggregation": "Count",
            "dimensions": [
              
            ],
            "metricValue": 4.0,
            "failingPeriods": {
              "numberOfEvaluationPeriods": 1,
              "minFailingPeriodsToAlert": 1
            },
            "linkToSearchResultsUI": "https://portal.azure.com#@11111111-1111-1111-1111-111111111111/blade/Microsoft_Azure_Monitoring_Logs/LogsBlade/source/Alerts.EmailLinks/scope/%7B%22resources%22%3A%5B%7B%22resourceId%22%3A%22%2Fsubscriptions%2F11111111-1111-1111-1111-111111111111%2FresourceGroups%2Frg-test-dev-001%2Fproviders%2Fmicrosoft.insights%2Fcomponents%2Ffunc-test-dev-eastus-001%22%7D%5D%7D/q/hoge%2Fhoge%3D%3D/prettify/1/timespan/2023-12-20T01%3a37%3a44.0000000Z%2f2023-12-20T01%3a42%3a44.0000000Z",
            "linkToFilteredSearchResultsUI": "https://portal.azure.com#@11111111-1111-1111-1111-111111111111/blade/Microsoft_Azure_Monitoring_Logs/LogsBlade/source/Alerts.EmailLinks/scope/%7B%22resources%22%3A%5B%7B%22resourceId%22%3A%22%2Fsubscriptions%2F11111111-1111-1111-1111-111111111111%2FresourceGroups%2Frg-test-dev-001%2Fproviders%2Fmicrosoft.insights%2Fcomponents%2Ffunc-test-dev-eastus-001%22%7D%5D%7D/q/hoge%2Fhoge%3D%3D/prettify/1/timespan/2023-12-20T01%3a37%3a44.0000000Z%2f2023-12-20T01%3a42%3a44.0000000Z",
            "linkToSearchResultsAPI": "https://api.applicationinsights.io/v1/apps/11111111-1111-1111-1111-111111111111/query?query=traces%20%0D%0A%7C%20where%20severityLevel%20%3E%202%0D%0A%7C%20project%20timestamp%2C%20message%2C%20severityLevel%2C%20operation_Id%2C%20appId%2C%20appName&timespan=2023-12-20T01%3a37%3a44.0000000Z%2f2023-12-20T01%3a42%3a44.0000000Z",
            "linkToFilteredSearchResultsAPI": "https://api.applicationinsights.io/v1/apps/11111111-1111-1111-1111-111111111111/query?query=traces%20%0D%0A%7C%20where%20severityLevel%20%3E%202%0D%0A%7C%20project%20timestamp%2C%20message%2C%20severityLevel%2C%20operation_Id%2C%20appId%2C%20appName&timespan=2023-12-20T01%3a37%3a44.0000000Z%2f2023-12-20T01%3a42%3a44.0000000Z"
          }
        ],
        "windowStartTime": "2023-12-20T01:37:44Z",
        "windowEndTime": "2023-12-20T01:42:44Z"
      }
    },
    "customProperties": {
      "includeSearchResults": "True"
    }
  }
}

Application InsightsのAPI実行結果

linkToFilteredSearchResultsAPIを実行すると以下のレスポンスをJSONで取得できる。

"linkToFilteredSearchResultsAPI": https://api.applicationinsights.io/v1/apps/11111111-1111-1111-1111-111111111111/query?query=traces 
| where severityLevel > 2
| project timestamp, message, severityLevel, operation_Id, appId, appName&timespan=2023-12-20T01:37:44.0000000Z/2023-12-20T01:42:44.0000000Z
{
  "tables": [
    {
      "name": "PrimaryResult",
      "columns": [
        {
          "name": "timestamp",
          "type": "datetime"
        },
        {
          "name": "message",
          "type": "string"
        },
        {
          "name": "severityLevel",
          "type": "int"
        },
        {
          "name": "operation_Id",
          "type": "string"
        },
        {
          "name": "appId",
          "type": "string"
        },
        {
          "name": "appName",
          "type": "string"
        }
      ],
      "rows": [
        [
          "2024-01-08T02:13:47.6814847Z",
          "Executed 'Functions.TeamsHttpTrigger' (Failed, Id=hoge, Duration=301ms)",
          3,
          "hoge",
          "hoge",
          "/subscriptions/hoge/resourcegroups/rg-001/providers/microsoft.insights/components/func-001"
        ],
        [
          "2024-01-08T02:13:48.0240301Z",
          "Executed 'Functions.TeamsHttpTrigger' (Failed, Id=hoge, Duration=112ms)",
          3,
          "hoge",
          "hoge",
          "/subscriptions/hoge/resourcegroups/rg-001/providers/microsoft.insights/components/func-igichatvision-dev-eastus-001"
        ],
        [
          "2024-01-08T02:13:49.6339781Z",
          "Executed 'Functions.TeamsHttpTrigger' (Failed, Id=hoge, Duration=104ms)",
          3,
          "hoge",
          "hoge",
          "/subscriptions/hoge/resourcegroups/rg-dev-001/providers/microsoft.insights/components/func-001"
        ]
      ]
    }
  ]
}

Microsoft Teamsに通知する際のアーキテクチャ構成

Azure MonitorのアラートをMicrosoft Teamsに通知する場合の構成は、以下の図になります。

  • Azure Monitorのアラートルールでアラートを検出
  • Azure MonitorのアクショングループでWebhookで通知用アプリ(Azure FunctionsやLogic Appsなど)に通知
  • 通知アプリで共通アラートスキーマからlinkToFilteredSearchResultsAPIのURLを取得し、Application Insightにリクエストを実行し、エラーログを取得
  • 通知用アプリからWebhookでTeamsに通知

アクショングループのWebhookではなく、通知用のアプリを間に設定する理由として、アクショングループの通知する場合はAzure Monitorの共通JSONアラートスキーマとして通知されるので、TeamsのWebhookへの直接の通知はサポート対象外になるため、通知用のアプリで共通JSONアラートスキーマをTeamsに通知する形式に加工してから、Webhookで通知しています。

上記のアーキテクチャは以下の手順で構築します。

  • Azure Monitorのアラートルールでアラートを通知する条件を設定
  • Azure Monitorのアクショングループで通知方法を設定 (WebhookでAzure Functions、Logic Appsなど通知用のアプリを設定)
  • Microsoft Teamsの通知対象チャネルで受信用のWebhookを作成 : 受信用のWebhookを作成
  • アクショングループのwebhookに指定した通知用のアプリにWebhookを設定

PythonでTeamsのチャネルに通知

PythonからTeamsのチャネルにWebhookで通知する場合、pymsteamsというライブラリを使用することで、通知ができるようになります。

https://pypi.org/project/pymsteams/

以下に、Azure FunctionsのHTTPトリガーでpyteamsを使ってTeamsのチャネルに通知するコードを記載しました。

import json
import logging
import traceback
from datetime import datetime

import azure.functions as func
import pymsteams
import requests
import os

TEAMS_WEBHOOK_URL = os.environ['TEAMS_WEBHOOK_URL']
APPLICATION_INSIGHTS_API_KEY = os.environ['APPLICATION_INSIGHTS_API_KEY']

def main(req: func.HttpRequest) -> func.HttpResponse:
    logging.info('Python HTTP trigger function to starting')
    body = req.get_body()
    headers = req.headers
    params = req.params
    response = ""
    status_code = 200

    try:
        logging.debug(f"Headers: {headers}")
        logging.debug(f"Body: {type(body)}{body}")
        body_decode = body.decode('utf-8')
        body_json = json.loads(body_decode)
        logging.info(f"Body json: {body_json}")

        data_json = body_json['data']

        alert_context_json = data_json['alertContext']
        condition_json = alert_context_json['condition']
        allOf_json = condition_json['allOf']

        # Application Insights APIに認証ヘッダーを付けてリクエストを送信する
        headers = {'x-api-key': APPLICATION_INSIGHTS_API_KEY}

        filtered_search_results_api_link = allOf_json[0]['linkToFilteredSearchResultsAPI']
        filtered_search_response_response = requests.get(filtered_search_results_api_link, headers=headers)
        filtered_search_response_response_json = filtered_search_response_response.json()
        logging.info(f"Response linkToFilteredSearch: {type(filtered_search_response_response_json)}: {filtered_search_response_response_json}")
        tables = filtered_search_response_response_json['tables']
        rows = tables[0]['rows']

        sections = {}

        for row in rows:
            sections["timestamp"] = row[0]
            sections["message"] = row[1]
            sections["severity_level"] = row[2]
            sections["operation_id"] = row[3]
            sections["app_id"] = row[4]
            sections["app_name"] = row[5]
            app_name = os.path.basename(sections["app_name"])
            title = f"{app_name} Application Insights Alert"
            timestamp = datetime.strptime(sections["timestamp"], '%Y-%m-%dT%H:%M:%S.%fZ')
            body = f"{timestamp} Send alert from {app_name} Application Insights"
            post_teams(TEAMS_WEBHOOK_URL, title, body, sections)

            logging.info(f"row: {sections}")

        response = f"alert count : {str(len(rows))} {str(rows)}"

    except Exception as e:
        status_code = 500
        response = traceback.format_exc()
        logging.error(f"Error: {e}", exc_info=True)

    return func.HttpResponse(response, status_code=status_code)

def post_teams(incoming_webhook_url, send_message_title, send_message_body, sections, link_url=None):
    """Teamsへメッセージを送信
    Args:
        send_message_title (str): メッセージのタイトル
        send_message_body (str): メッセージの本文
        sections (dict): セクション
        link_url (str): リンク先のURL
    Returns:
        None
    """

    logging.info(f"post_teams: {incoming_webhook_url}")
    if incoming_webhook_url:
        logging.info(f"post_teams: {send_message_title}")
        # メッセージを送信
        teams = pymsteams.connectorcard(incoming_webhook_url)
        teams.title(send_message_title)
        teams.text(send_message_body)
        if link_url:
            teams.addLinkButton("リンク", link_url)
        card_section = pymsteams.cardsection()
        for key in sections.keys():
            card_section.addFact(key, sections[key])
        teams.addSection(card_section)
        teams.send()

おわりに

この記事ではAzure MonitorのアラートをMicrosoft Teamsに通知する方法を紹介しました。
この記事がエンジニアの方の参考になれば、幸いです。