Application Insights による Python アプリ (Flask) の監視設定 (OpenTelemetry版)

# 本記事について

本記事では、Azure の APM サービスである [**Application Insights**](https://learn.microsoft.com/ja-jp/azure/azure-monitor/app/app-insights-overview) を使って、Python ベースのウェブアプリケーションの監視の設定について見ていきます。特に、サンプルとして Flask のアプリケーションを使って確認していきます。

# Azure Application Insights とは？

Application Insights は Azure Monitor の一機能として、APM (Application Performance Management) サービスを担っています。

https://learn.microsoft.com/ja-jp/azure/azure-monitor/app/app-insights-overview

アプリケーションの監視について、ログ収集、メトリック管理、アプリケーションのマップの作成、分散トレーシング、可用性テストなどを提供しています。

Application Insights そのものについては、下記の記事が詳細にご紹介されていらっしゃいます。

https://tech-lab.sios.jp/archives/23948

この Application Insights の利用方法として、Azure App Service や Azure Functions などでプラットフォーム側で有効化する方法と、.NET や Java, Javascript や Python の SDK を利用して組み込む方法があります。

本記事では後者の SDK を利用する方式で、Python アプリケーションを監視する方法を見ていきます。

# Application Insights と Python

Application Insights の Python 監視は、[**古い方式 (OpenCensus を利用する方式)**](https://learn.microsoft.com/ja-jp/azure/azure-monitor/app/opencensus-python) と[**新しい方式 (OpenTelemetry ベースの方式)**](https://learn.microsoft.com/ja-jp/azure/azure-monitor/app/opentelemetry-enable?tabs=python) があります。[**前者が2024年9月でのサポート終了がアナウンス**](https://azure.microsoft.com/ja-jp/updates/python-opencensus-retirement/)されているため、本記事では後者の OpenTelemetry を用いた方式での構成をご紹介していきます。

# コードの設定と Application Insights 上のデータの確認

ここからは、[**Appliction Insights の Python SDK**](https://github.com/Azure/azure-sdk-for-python/tree/main/sdk/monitor/azure-monitor-opentelemetry) を使って、同コードを書き、どのようにデータが格納されるかを見ていきます。

https://github.com/Azure/azure-sdk-for-python/tree/main/sdk/monitor/azure-monitor-opentelemetry

## 1. 基本設定

Appliction Insights の Python SDK では、[**下記のモジュール**](https://pypi.org/project/azure-monitor-opentelemetry/)を利用します。

```
pip install azure-monitor-opentelemetry
```

そして、Application Insights の接続文字列を利用して起動します。

```python
from azure.monitor.opentelemetry import configure_azure_monitor
appInsightsConnectionString = "<Application Insights の接続文字列>"
configure_azure_monitor(
    connection_string=appInsightsConnectionString,
)
```

また、環境変数に
```
OTEL_RESOURCE_ATTRIBUTES="service.namespace="<名前空間名>,service.instance.id=<インスタンス名>"
OTEL_SERVICE_NAME="<サービス名>"
```
をセットしておきます。(Linux 環境の場合は export <環境変数名>=<値> など。) 

この値が、ログの中の `cloud_RoleName` や `cloud_RoleInstance` の値になったり、アプリケーションマップの表示名になったりします。

さらに、環境変数では、サンプリング率を決めて、収集データ量を減らすことができます。（ログとメトリックはこのサンプリングの対象外です。)

```
OTEL_TRACES_SAMPLER_ARG=0.1 (0 (0%) ~1 (100%)の値を入れる)
```

## 2. logger と traces テーブル

`configure_azure_monitor()` で Application Insights 接続を有効化すると、あとは logger を使ってログ記録を行うと、自動的に Application Insights にログが記録されます。OpenCensus の時と異なり、`logger.addHandler()` でハンドラーを追加する必要はありません。

```python
import logging
logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)
logger.info("<出力したいログ>")
```

とすると、`"<出力したいログ>"` が Application Insights の `traces` テーブルに入ります。ログの中身 (上記では `"<出力したいログ>"`)は、traces テーブルの message 列にて確認できます。

```:Kusto
traces 
| sort by timestamp desc 
| project timestamp, cloud_RoleName, cloud_RoleInstance, severityLevel, message,
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276225/168a2946-b49c-bea2-2ab8-112fcc4b066a.png)

また、上記 Kusto クエリでは、cloud_RoleName と cloud_RoleInstance を出力していますが、それぞれ、`OTEL_RESOURCE_ATTRIBUTES` と `OTEL_SERVICE_NAME` で設定した値が入ります。

```
cloud_RoleName = <service.namespaceの値>.<OTEL_SERVICE_NAMEの値>
cloud_RoleInstance = <service.instance.idの値>
```

なお、同ログは Application Insights が利用している Log Analytics ワークスペースからも検索ができます。ただし、**同じログですがテーブル名や列名が異なる**点に注意が必要です。traces テーブルは [**AppTraces テーブル**](https://learn.microsoft.com/ja-jp/azure/azure-monitor/reference/tables/apptraces)となり、列名も微妙にそれぞれ異なります。(timestamp が TimeGenerated になっていたりなど・・・)

例えば、Log Analytics ワークスペースでは、下記のようなクエリを実行できます。

```:Kusto
AppTraces 
| sort by TimeGenerated desc
| project TimeGenerated, AppRoleName, AppRoleInstance, SeverityLevel, Message
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276225/7ff71cb8-6dc5-bd0d-bf14-b530a612a03e.png)

### (補足) Application Insights と Log Analytics のテーブル名とスキーマについて

下記のセクションでご紹介していく Application Insights のテーブルも、`traces` テーブルと同じく、Log Analytics ではそれぞれ名前が異なっています。Log Analytics 側のテーブルのリファレンスは下記にてまとめられています。

https://learn.microsoft.com/ja-jp/azure/azure-monitor/reference/supported-logs/microsoft-insights-components-logs

### (補足) ログをより安価に保存するために - 基本ログ (Basic Logs) とワークスペース変換

traces ログが大量に入ってしまい、コストが気になる場合は下記の方法にて、ログ料金を減らすことができる可能性があります。

#### 基本ログ (Basic Logs) の活用

Log Analytics には、通常の分析ログ (Analytics Logs) に加え、機能や保存期間に制約があるもののより安価に保存できる[**基本ログ (Basic Logs)**](https://learn.microsoft.com/ja-jp/azure/azure-monitor/logs/basic-logs-configure?tabs=portal-1) というログの方式があります。

https://learn.microsoft.com/ja-jp/azure/azure-monitor/logs/basic-logs-configure?tabs=portal-1

[**`AppTraces (traces)` テーブルはこれに対応している**](https://learn.microsoft.com/ja-jp/azure/azure-monitor/logs/basic-logs-configure?tabs=portal-1#supported-tables)ため、テーブル単位で設定を変えることで基本ログとして扱うことができます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276225/5c286ea9-8e87-ee10-357e-ea499d6888bf.png)

また、アーカイブ機能と組み合わせることで、より長期の保持にも対応できます。

#### ワークスペース変換

Log Analytics のワークスペース変換機能を利用することで、取り込み時にフィルターをかけログ量を減らすことができます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276225/40867393-8991-f280-3730-2b92978d4855.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276225/8a809387-63f0-7428-fcef-7a79b6de4822.png)

ワークスペース変換機能自体の詳細については、下記も併せてご参照ください。

https://qiita.com/YoshiakiOi/items/f1bae07e075382f91a94


## 3. HTTP のリクエストと requests テーブル (Flask の場合)

OpenTelemetry の Python 用のモジュールでは、Flask, FastAPI, Django 用に専用のモジュールが用意されています。これを利用することで、**HTTP(S) のリクエスト**を記録することができます。

https://learn.microsoft.com/ja-jp/azure/azure-monitor/app/opentelemetry-add-modify?tabs=python

Flask の場合、`app = Flask(__name__)` の後に、下記を追加することで有効化できます。

```python
from opentelemetry.instrumentation.flask import FlaskInstrumentor
FlaskInstrumentor().instrument_app(app)
```

なお、FastAPI の場合は下記になります。

```python
from opentelemetry.instrumentation.flask import FastAPIInstrumentor
FastAPIInstrumentor().instrument_app(app)
```

それぞれ、pip install などで、下記モジュールを事前にインストールしておく必要があります。

```txt
opentelemetry-instrumentation-flask # Flask
opentelemetry-instrumentation-fastapi # FastAPI
```

すると、この Flask アプリケーションへの HTTP(S) 要求が、Application Insights の `requests` テーブル (Log Analytics ワークスペースの `AppRequests` テーブル) に格納されます。

```:Kusto
requests
| sort by timestamp desc
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276225/5bbc17c4-c7ec-a93f-de5e-c2a161aaeef3.png)

このテーブルには、HTTP(S) 要求に関して、時刻、メソッド、URL, ステータスコード、継続時間、IPアドレス、ブラウザーなど、基本的な情報が多く含まれています。

## 4. Exception (例外) と exceptions テーブル

Exception は、`configure_azure_monitor()` で Application Insights 接続を有効化すると、Application Insights の `exceptions` テーブル (Log Analytics の `AppExceptions` テーブル) に自動的に格納されます。

```:Kusto
exceptions
| sort by timestamp desc
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276225/64656d35-f934-ba0a-f058-29c30238ae34.png)

## 5. tracer と パフォーマンス・障害・dependencies テーブル

次に、OpenTelemetry の tracer について見ていきます。`configure_azure_monitor()` で Application Insights 接続を有効化したのち、`tracer` を起動してトレース情報を取得します。

```python
from opentelemetry import trace
tracer = trace.get_tracer(__name__)
```

また、トレースの単位となる、span を必要に応じて作成します。例として、簡単な Flask のアプリケーションのサンプルを挙げてみます。こちらは、今までご紹介してきた、logger や Flask 用の OpenTelemetry のモジュールも含めた形になっています。

下記のコードでは、index() メソッドの実行について、１つ span を作成しています。このメソッド実行をひとつの単位としてトレースするためです。

:::note info
ただし、Flask や FastAPI の場合は、Flask/FastAPI 用の OpenTelemetry のインストルメンテーションの中で、各受信に対して tracer を自動的に span context を作成するため、`with tracer.start_as_current_span("<span 名>") as <span 変数名>:` は無くても trace は動作します。)

Child の Span を定義したい場合に、メソッド内の一部のコードを囲う形で `tracer.start_as_current_span()` を使うこともできます。
:::


```python
# 利用モジュールのインポート
from flask import Flask
from azure.monitor.opentelemetry import configure_azure_monitor
from opentelemetry import trace
from opentelemetry.instrumentation.flask import FlaskInstrumentor
import logging

# Flaskの初期化
app = Flask(__name__)

# OpenTelemetry の初期化
appInsightsConnectionString = "<Application Insights の接続文字列>"
configure_azure_monitor(
    connection_string=appInsightsConnectionString,
)
tracer = trace.get_tracer(__name__)
FlaskInstrumentor().instrument_app(app)

# loggerの初期化
logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)

# index ページの表示
@app.route('/')
def index():
    with tracer.start_as_current_span("index-span") as index_span:
        logger.info("Index page was accessed")
        logger.info(f"Span: {index_span}")
        return 'Index Page'

if __name__ == "__main__":
    app.run(debug=False,host='0.0.0.0',port=5000)
```

この状態で、index ページにアクセスしてみます。その記録を、Application Insights のパフォーマンスページ (エンドツーエンドトランザクションの詳細) で確認すると、下記のように見ることができます。GET のリクエストが来て、ステータスコード200でレスポンスしていることが見て取れます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276225/8be422e3-b267-f3b5-ab84-ad4382ecf8bf.png)

Log Analytics アイコンをクリックし、この操作 ID (operation_Id) に紐づくログを検索すると、下記のようにそれぞれのログを確認できます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276225/b50cb911-1451-4925-948e-d5a8fc06c1d3.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276225/c52841ef-45da-662c-62c1-c7a16554d10f.png)

上記の例では、Application Insights のトレーシングで依存関係を記録する `dependencies` テーブル (`AppDependencies` テーブル) には1レコードのみ (index-span の起動結果) が入っています。単に HTTP のリクエストにレスポンスしているだけであることが原因です。

また、`requests` ログや `traces` ログにも同じ操作IDが付与されていることもわかります。これにより、起点となる HTTP アクセスログや、開発者が logger で記録したログも一貫した操作の記録として扱うことができます。

今回、`logger.info(f"Span: {index_span}")` で span の情報もログとして出力しているので、これを見てみます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276225/0deb77d2-f7bb-d39d-4d7d-f9475bb37feb.png)

すると、`trace_id` に入っている値が`operation_Id`, `span_id` の値が `operation_ParentId` にそれぞれ格納されていることが分かります。[span_id の値が、operation_ParentId に入っているのは、このログ自体 (trace ログのこのレコード自体) は span ではないため、自身を含んでいる Span (=親 Span) の値を、operation_ParentId となっているためです。span_id 自体を記録しているレコードは、Id 列にその値が入ります。](https://github.com/microsoft/ApplicationInsights-Java/issues/3486) OpenTelemetry の Span の仕組みを Application Insights では (列名は違うものの) そのまま利用することができ、Span の親子関係も捉えることができます。

ただ、これだけではトレーシングとして面白みがないので、リクエストがあったときに、Azure Cosmos DB に値を記録するケースで見ていきたいと思います。(下記のサンプルは Cosmos DB の設定などを除いたコードです。実際には 接続文字列、DB 名、コンテナー名の設定などを事前にしていることを想定しているものになります。)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276225/e43e1584-10c4-d700-f22d-a1c66608c6b1.png)


```python
@app.route('/hello/')
@app.route('/hello/<name>')
def hello(name=None):
    with tracer.start_as_current_span("hello-span") as hello_span:
        logger.info("Hello page was accessed")
        logger.info(f"Span: {hello_span}")
        # id に GUID を入力
        id = str(uuid.uuid4())
        if not name:
            name1 = "NULL"
        else:
            name1 = name
        new_item = {
            "id": id,
            "name": name1
        }
        try:
            # CosmosDB に接続
            client = CosmosClient.from_connection_string(connectionString)
            database = client.get_database_client(dbName)
            container = database.get_container_client(containerName)
            logger.info(f"Connection to CosmosDB was initialized, database: {database}, container: {container}")

            # CosmosDB に新しい item を作成
            created_item = container.upsert_item(new_item)
            logger.info("New item was created on Cosmos DB")
        except Exception as e:
            logger.error(e)
            
        return f'Hello, {name}!'
```

この Hello ページにアクセスした時の記録を Application Insights で確認すると、今度はトランザクションの hello-span の配下に Cosmos DB へのアクセスが記録されていることが分かります。(さらに、Child の Span がひとつ自動的に作成されています。)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276225/e18c2018-3497-b291-8d41-7202e674c967.png)

dependencies テーブルのレコードにも、同じ operation_Id に紐づくアクティビティの中で、Cosmos DB へのアクセスがあったことが記録されています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276225/4f15020e-290c-99fd-8134-9335ad04d451.png)

Cosmos DB へのアイテム作成は、別の Child Span として operation_ParentId が記録されています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276225/8ce6fbe2-e300-0eb7-6321-50f50b2af6bf.png)

また、上記のパフォーマンス以外にも、Application Insights の障害ページで、特定のエラーについてトランザクションをエンドツーエンドの詳細を確認しながら追いかけることもできます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276225/fb003116-84fe-3594-c08a-b39331fe326b.png)

## 6. アプリケーションマップ

Application Insights で一番目を引くのが、アプリケーションマップです。最後にこちらをご紹介します。


OpenTelemetry 経由で取得したアプリケーションの情報をマップとして可視化してくれます。例えば、上記の Flask アプリケーションで、Cosmos DB にアクセスしたものは、下記のように可視化されています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276225/50d4ac20-9f18-b19e-0484-5f6f4c6cb58f.png)

このマップのアプリの表記名も、ログと同じく環境変数で設定した、`<service.namespaceの値>.<OTEL_SERVICE_NAMEの値>` になっています。そして上記でご紹介した各情報へのリンクも表示されています。

# 最後に

本記事では、Application Insights (Python, OpenTelemetry ベース版) を用いて、Python アプリケーションを監視する際に、どういった構成が必要で、構成後にどのようにデータを確認できるかをご紹介しました。

本稿が、これから Azure 上で Python のアプリケーションを稼働される際に、監視に Application Insights を採用する一助となれば幸いです。

なお、別のサンプルとして、Azure OpenAI Service を利用した Python アプリケーションの監視と、FastAPI を Azure Functions にホストした際の Application Insights 利用について、それぞれ下記にてご紹介しております。

https://qiita.com/YoshiakiOi/items/e7975f454772b4764543

https://qiita.com/YoshiakiOi/items/4ba1b5d4830d2648e009#%E8%A3%9C%E8%B6%B3-application-insights-%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6

＊本稿は、個人の見解に基づいた内容であり、所属する会社の公式見解ではありません。また、いかなる保証を与えるものでもありません。正式な情報は、各製品の販売元にご確認ください。