OpenAI Agents SDKのTrace情報をApplication Insightsへ送信・可視化する！(MCP実行例付き)

## はじめに

OpenAI Agents SDKを利用した際にTrace情報をAzureのApplication Insightsに送信する方法を調査したので共有します。  
従来のChatCompletions APIでは、Application Insightsに送信する方法として[Azure Monitor OpenTelemetry Distro](https://learn.microsoft.com/ja-jp/azure/azure-monitor/app/opentelemetry-configuration?tabs=python) + [OpenTelemetry OpenAI Instrumentation](https://pypi.org/project/opentelemetry-instrumentation-openai-v2/)を組み合わせることで送信できたのですが、2025年4月9日時点ではResponses APIには対応しておらず送信できませんでした。  
そこで今回、[こちら](https://techcommunity.microsoft.com/blog/azure-ai-services-blog/monitor-openai-agents-sdk-with-application-insights/4393949)のAzure AI Service Blogをベースに、Pydantic Logfire SDK + OpenTelemetry Collector経由で実装しました。なお元記事より下記の点を変更しています。

* span名の修正をソースを変更するのではなく関数をフックする形で実現
* MCP(Model Context Protocol)サーバーを実行した結果の記録例

## OpenTelemetry Collectorの設定と実装例と実行結果

今回検証に利用した各OSSのバージョンは以下の通りです。  

* OpenAI Agents SDK: v0.0.7
* Logfire: v3.12.0
* OpenTelemetry Collector: v0.123.1

### OpenTelemetry Collector のセットアップ

OpenAI Agents SDKのトレース情報を収集してApplication Insightsへ転送するためにOpenTelemetry Collectorを設定します。
ここでは下記のGitHubのリリースよりOpenTelemetry Collector Contribを実行ファイルをダウンロードしローカルで実行します。
なおContribリリース以外は、Azure Monitor Exporterをサポートしていないのでご注意ください。
[元記事](https://techcommunity.microsoft.com/blog/azure-ai-services-blog/monitor-openai-agents-sdk-with-application-insights/4393949)では、Dockerコンテナでの実行や、AKS、Azure Container Appsに関しても記載されていますので、各環境に応じて使い分けください。  

[Releases open-telemetry/opentelemetry-collector-releases](https://github.com/open-telemetry/opentelemetry-collector-releases/releases)

```shell
> ./otelcol-contrib --config=otel-collector-config.yaml
```

上記コマンドで指定した設定ファイル`otel-collector-config.yaml`は以下の通りで、Application Insightsにデータを転送する基本的な構成になっています。  
Azure Monitor Exporterの`connection_string`は各環境に応じて変更してください。

```yaml:otel-collector-config.yaml
receivers:
  otlp:
    protocols:
      http:
        endpoint: "0.0.0.0:4318"

exporters:
  azuremonitor:
    connection_string: "InstrumentationKey=your-instrumentation-key;IngestionEndpoint=https://your-region.in.applicationinsights.azure.com/"
    maxbatchsize: 100
    maxbatchinterval: 10s
  debug:
    verbosity: basic

service:
  pipelines:
    traces:
      receivers: [otlp]
      exporters: [azuremonitor, debug]
```

### OpenAI Agents SDKとLogfireの実装例

OpenAI Agents SDKとLogfireの実装例は以下の通りで、[OpenAI Agents SDKのサンプル](https://github.com/openai/openai-agents-python/tree/main/examples/mcp/filesystem_example)と、元記事の[公開コード](https://github.com/hieumoscow/azure-openai-agents/blob/main/main.py)を組み合わせたコードになっています。
また実装時のポイント2点については後述させて頂きます。

```python
import asyncio
import os
import shutil

from openai import AsyncAzureOpenAI
from agents import Agent, Runner, set_default_openai_client, trace
from agents.mcp import MCPServer, MCPServerStdio
from azure.identity import DefaultAzureCredential, get_bearer_token_provider
from dotenv import load_dotenv
import logfire

from wrapt import wrap_function_wrapper
from logfire._internal.formatter import logfire_format
from logfire._internal.scrubbing import NOOP_SCRUBBER
from agents.tracing.processors import default_processor

load_dotenv()

# OpenTelemetry Collector経由でApplication Insightsに送信
os.environ["OTEL_EXPORTER_OTLP_TRACES_ENDPOINT"]= "http://127.0.0.1:4318/v1/traces"

# Spanの呼び出しをフックし、Span名称を書き換え　－ポイント１
def span_wrapper(wrapped, instance, args, kwargs):
    kwargs["_span_name"] = logfire_format(args[0], kwargs, NOOP_SCRUBBER)
    return wrapped(*args, **kwargs)

wrap_function_wrapper(
    module="logfire._internal.main",
    name="Logfire.span",
    wrapper=span_wrapper,
)

# Logfireを初期化
logfire.configure(
    service_name='my-agent-service',
    send_to_logfire=False,
    distributed_tracing=True)
logfire.instrument_openai_agents()

# 背後でOpenAIにTrace情報を送信するProcessorが動作しているので停止する　－ポイント２
default_processor().shutdown()

# Azure OpenAIクライアントを初期化
credential = DefaultAzureCredential()
deployment_name = os.getenv("AZURE_OPENAI_DEPLOYMENT")
openai_client = AsyncAzureOpenAI(
    api_version=os.getenv("AZURE_OPENAI_API_VERSION"),      # 2025-03-01-preview or later
    azure_endpoint=os.getenv("AZURE_OPENAI_ENDPOINT"),
    azure_deployment=os.getenv("AZURE_OPENAI_DEPLOYMENT"),
    azure_ad_token_provider=get_bearer_token_provider(credential, "https://cognitiveservices.azure.com/.default"),
)

# OpenAI Agents SDKにクライアントをセット
set_default_openai_client(openai_client)

async def run(mcp_server: MCPServer):
    agent = Agent(
        name="Assistant",
        instructions="Use the tools to read the filesystem and answer questions based on those files.",
        mcp_servers=[mcp_server],
        model=deployment_name
    )

    # List the files it can read
    message = "Read the files and list them."
    print(f"Running: {message}")
    result = await Runner.run(starting_agent=agent, input=message)
    print(result.final_output)

async def main():
    # トレースを開始
    with trace(workflow_name="OpenAI Agents Demo"):
        current_dir = os.path.dirname(os.path.abspath(__file__))
        samples_dir = os.path.join(current_dir, "sample_files")

        async with MCPServerStdio(
            name="Filesystem Server, via npx",
            params={
                "command": "npx",
                "args": ["-y", "@modelcontextprotocol/server-filesystem", samples_dir],
            },
        ) as server:
            await run(server)


if __name__ == "__main__":
    # npxがインストールされているか確認
    if not shutil.which("npx"):
        raise RuntimeError("npx is not installed. Please install it with `npm install -g npx`.")

    asyncio.run(main())
```

### 実行結果

Azure Application Insightsのトランザクションの検索からトレース情報を可視化した結果は以下の通りです。
エージェントの名称やMCPサーバー名、モデル名等がいつ、どのように呼び出されたか時系列で確認できます。  
またMCPサーバーのどのツールを実行したかについては「Function: (ツール名)」として出力され、入出力結果についても記録されているのが確認できました。

![Application Insightsでの記録結果](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/695104/5dc4994e-75f0-417d-9645-45fa15cf8151.png)


## コードの解説

### ポイント１：Logfire.spanの呼び出しをフックし名称を書き換え

ここではOpenTelemetryが内部で使用している`wrapt`を利用し`Logfire.span`関数の呼び出しをフックし、Logfireのテンプレート文字列の書き換え処理を行っています。
これはLogfireではメッセージとしてテンプレート形式をサポートしていますが、Application Insightsではサポートされていないため発生します。関数をラップしなかった場合のApplication Insightsでの表示は以下のキャプチャの通りで、ラップしない状態ではテンプレート文字列がそのまま表示され、エージェント名やツール名、モデル名が表示されず確認し辛くなっています。

![関数をラップしなかった場合のApplication Insights](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/695104/b598b563-9728-40ae-bf5f-dada78ebebae.png)

<details><summary>(参考)ラップした場合のデメリットとその対策</summary>

`Logfire.span`関数をラップした結果、以下のキャプチャの通り、全てユーザコードとして判定され、呼び出し元が`span_wrapper`として記録されます。

![Span関数をラップした際の記録例](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/695104/30926ffb-deb1-4717-a30f-770fee6fda0f.png)


この対策として、①ユーザコードの判定ロジックをラップして対処する方法と、②`Logfire.span`関数をラップするロジックを別ファイルとして作成しLogfireの[`add_non_user_code_prefix`](https://logfire.pydantic.dev/docs/reference/api/logfire/#logfire.add_non_user_code_prefix)を用いてユーザコードの対象外とする方法の2つがあります。
ここでは前者のユーザコードの判定ロジックをラップして対処する方法を以下に示します。
こちらのコードをサンプルコードの`wrap_function_wrapper`の後に記載ください。
内容はシンプルで、呼び出し元が`span_wrapper`の場合はユーザコードとしないという内容になっています。

``` python
def is_user_code_wrapper(wrapped, instance, args, kwargs):
    return not (wrapped(*args, **kwargs), args[0].co_name == "span_wrapper")

wrap_function_wrapper(
    module="logfire._internal.stack_info",
    name="is_user_code",
    wrapper=is_user_code_wrapper,
)
```

</details>

### ポイント２：OpenAI Agents SDKのTrace情報送信スレッドの停止

OpenAI Agents SDKのv0.0.7では、Pythonのモジュールをインポートした時点で強制的にOpenAIへのTrace情報の送信スレッドが開始されてしまいます。
このため`logfire.instrument_openai_agents()`にて、ログ記録モジュールをOpenAI Agents SDKからLogfireに切り替えたとしても、Trace情報送信処理が実行されてしまいエラーが発生します。
これを抑止するため、OpenAI Agents SDKの`default_processor()`より`BatchTraceProcessor`インスタンスを取得、`shutdown`メソッドにより送信スレッドを停止し、エラーの発生を抑止しています。

※参考：[Processors - OpenAI Agents SDK](https://openai.github.io/openai-agents-python/ref/tracing/processors/#agents.tracing.processors.BatchTraceProcessor)

### [参考] Azure AI Foundry(旧Azure AI Studio)のトレース機能による可視化について

当初はAzure AI Foundry、またはAzure ML Studio上でTrace情報を収集・可視化できないか調査を開始しましたが、Azure ML Studio(Azure AI Foundryプロジェクト含)では、残念ながらMLflowのTrace APIをサポートしておらず、またAzure AI Foundry SDK ([Azure AI Inference client library](https://pypi.org/project/azure-ai-inference/))は独自クライアントとなっておりOpenAI Agents SDKとの互換性がないという課題があり利用については断念しました。
またAzure AI Foundry上ではTraceについては一覧表示されるものの、互換性がないためか具体的な内容については以下のキャプチャの通り表示されませんでした。

![Azure AI Foundry上の表示](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/695104/a6ba5aa9-1973-4ebc-a682-82314cf82d6c.png)

## 終わりに

LLM ObservabilityについてOpenTelemetryやMLflowが取り組んでいるものの中々スタンダードと呼べるものが出てこず、[OpenAI Agents SDKのTracing](https://openai.github.io/openai-agents-python/tracing/#external-tracing-processors-list)に記載されているTracing Processorを見ても独自に実装しているのが実情のようです。
今回AzureネイティブなサービスでOpenAI Agents SDKの可視化が可能か調査しましたが、Application Insightsを利用して何とか可視化出来るのが限界でした。
今後生成AIのエージェントやMCPが普及するにつれて、LLM Observabilityだけでなく分散Tracingの重要性も高まっていくと思いますので、引き続きウォッチしていきたいと思います。

## 参考記事

* [Monitor OpenAI Agents with Azure Application Insights](https://techcommunity.microsoft.com/blog/azure-ai-services-blog/monitor-openai-agents-sdk-with-application-insights/4393949)
* [Tracing - OpenAI Agents SDK](https://openai.github.io/openai-agents-python/tracing/)
* [mcp/filesystem_example at main - openai/openai-agents-python](https://github.com/openai/openai-agents-python/tree/main/examples/mcp/filesystem_example)