11
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Last updated at Posted at 2024-03-08

本記事について

本記事では、Azure の APM サービスである Application Insights を使って、Python ベースのウェブアプリケーションの監視の設定について見ていきます。特に、サンプルとして Flask のアプリケーションを使って確認していきます。

Azure Application Insights とは?

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

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

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

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

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

Application Insights と Python

Application Insights の Python 監視は、古い方式 (OpenCensus を利用する方式)新しい方式 (OpenTelemetry ベースの方式) があります。前者が2024年9月でのサポート終了がアナウンスされているため、本記事では後者の OpenTelemetry を用いた方式での構成をご紹介していきます。

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

ここからは、Appliction Insights の Python SDK を使って、同コードを書き、どのようにデータが格納されるかを見ていきます。

1. 基本設定

Appliction Insights の Python SDK では、下記のモジュールを利用します。

pip install azure-monitor-opentelemetry

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

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_RoleNamecloud_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() でハンドラーを追加する必要はありません。

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

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

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

なお、同ログは Application Insights が利用している Log Analytics ワークスペースからも検索ができます。ただし、同じログですがテーブル名や列名が異なる点に注意が必要です。traces テーブルは AppTraces テーブルとなり、列名も微妙にそれぞれ異なります。(timestamp が TimeGenerated になっていたりなど・・・)

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

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

image.png

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

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

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

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

基本ログ (Basic Logs) の活用

Log Analytics には、通常の分析ログ (Analytics Logs) に加え、機能や保存期間に制約があるもののより安価に保存できる基本ログ (Basic Logs) というログの方式があります。

AppTraces (traces) テーブルはこれに対応しているため、テーブル単位で設定を変えることで基本ログとして扱うことができます。

image.png

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

ワークスペース変換

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

image.png

image.png

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

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

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

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

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

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

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

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

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

このテーブルには、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

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

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

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

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

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

ただし、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() を使うこともできます。

# 利用モジュールのインポート
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

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

image.png

image.png

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

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

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

image.png

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

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

image.png

@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

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

image.png

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

image.png

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

image.png

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

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

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

image.png

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

最後に

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

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

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

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

11
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?