手動計装へのステップアップ
自動計装の手軽さはご理解いただけたかと思いますが、一方で、自動計装には限界があります。
自動計装に対応していないライブラリやフレームワークに関しては取得できるテレメトリーは限定的になってしまいます。また、自動計装されたとしてもトラブルシューティングなどで必要になる情報が必ず含まれるわけではありません。アプリケーションのロジックは業務仕様やビジネスルール、ドメイン知識などに基づいて作られており、その業務仕様やビジネスルールなどは要件によって異なるものである以上、自動計装のような汎用的な手法だけでは、痒いところに手が届かない部分が出てきます。これに対処していくには、手動計装にステップアップしていく必要があります。[3]
例えば、あるユーザーからクレームがあった場合に、そのユーザーが実施した処理を処理順に抽出し、その時にユーザーが入力した情報や出力されたエラーなどをすぐに確認できれば、問題解決はスムーズに進むことでしょう。
今回は、このサンプルアプリケーションでユーザー登録を実施する際に、ユーザが入力したEmailアドレスとユーザ名をスパンタグで確認できるように計装を加えていきます(なお、以下でいくつかのコードを参照しますが、パスはクローンしたコードレポジトリのディレクトリからの相対パスです)。
手動計装の基本的なアプローチは、(1)OpenTelemetry API、SDK をインポートし、(2)計装コードを追加する(トレーサーの取得・初期化、スパンの生成・設定、Attributeの追加・設定など)というものです。
では、やってみましょう。
今回のサンプルアプリケーションにおいて、“Register User” 処理が “POST /api/users” で実行されることはSwaggerを見ると一目瞭然です。が、これらが明確でない場合は、一度自動計装の状態でその処理を動かしてみるのがよいでしょう。実際に、Register User処理を一度実施してみると、オブザーバビリティバックエンド側にトレースが取得され、実行された処理内容が確認できるはずです。
詳細なウォーターフォール図も一度見ておきましょう。バックエンドのDBに対してINSERT処理などを行っているようです。
さて、“POST /api/users” というエンドポイントに関するルート定義を、実際のアプリケーションコードから読み解いていきます。
conduit/api/router.py
というファイルがあるのでこれを開いてみると、/users
は authentication.router
に関連づいているようです。
router.include_router( router=authentication.router, tags=["Authentication"], prefix="/users" )
conduit/api/routes/authentication.py
を見てみると、POST処理でregister_user
という関数が定義されています。
from fastapi import APIRouter from conduit.api.schemas.requests.user import UserLoginRequest, UserRegistrationRequest from conduit.api.schemas.responses.user import ( UserLoginResponse, UserRegistrationResponse, ) from conduit.core.dependencies import DBSession, IUserAuthService router = APIRouter() @router.post("", response_model=UserRegistrationResponse) async def register_user( payload: UserRegistrationRequest, session: DBSession, user_auth_service: IUserAuthService, ) -> UserRegistrationResponse: """ Process user registration. """ user_dto = await user_auth_service.sign_up_user( session=session, user_to_create=payload.to_dto() ) return UserRegistrationResponse.from_dto(dto=user_dto) // // 以下省略 //
ユーザーが “/api/users” にPOSTリクエストを実施すると、register_user
関数が、ユーザーが指定した情報を受け取り、sign_up_user処理を実施して、結果をクライアントに返却しているようです。
payload: UserRegistrationRequests
にユーザーが指定した情報が入っているはずで、これは import 句を見るとどうやらconduit.api.schemas.responses.user
に定義されているようなので、これも事前に確認しておきましょう。
conduit/api/schemas/responses/user.py
を開くと、以下のようにclassが定義されています。
class UserRegistrationData(BaseModel): email: str password: str username: str // 中略 class UserRegistrationRequest(BaseModel): user: UserRegistrationData def to_dto(self) -> CreateUserDTO: return CreateUserDTO( username=self.user.username, email=self.user.email, password=self.user.password, )
UserRegistrationRequest
クラスでuserが定義されていて、そのuserは、UserRegistrationData
クラスを参照しているようです。UserRegistrationData
に含まれるemail, usernameなどを参照してスパンタグに埋め込むことができると良さそうです。
では、計装ライブラリを追加していくところからやっていきましょう。
まずは、必要となるライブラリをrequirements.txt
に追加しておきます。
追加するのは以下の2つです。
opentelemetry-api opentelemetry-sdk
次に、ライブラリをインポートします。
conduit/api/routes/authentication.py
に以下を追加します。
from opentelemetry import trace
さらに、ユーザー登録処理にて、ユーザーが指定した情報を取得できるようにコードを追加します。
@router.post("", response_model=UserRegistrationResponse) async def register_user( payload: UserRegistrationRequest, session: DBSession, user_auth_service: IUserAuthService, ) -> UserRegistrationResponse: """ Process user registration. """ # 現在のスパンを取得 current_span = trace.get_current_span() # スパン属性としてユーザーのEmailとユーザ名を追加 current_span.set_attribute("payload.email", payload.user.email) current_span.set_attribute("payload.username", payload.user.username) user_dto = await user_auth_service.sign_up_user( session=session, user_to_create=payload.to_dto() ) return UserRegistrationResponse.from_dto(dto=user_dto)
“Process user registration”というコメントの直後、current_span = trace.get_current_span()
で、この関数を実施する際に生成されているアクティブなスパンを取得しています。
さらにその取得したアクティブなスパンに対してcurrent_span.set_attribute()
によって、属性名(key) “payload.email”および“payload.username” に対して、値(value) としてpayload.user.email
およびpayload.user.username
の変数の値を参照して設定しています。
ファイルを保存して、アプリケーションを起動しなおします。
SwaggerでRequest Bodyを指定して登録を実施してみましょう。
うまくいけば、HTTP=200で登録が成功するはずです。
オブザーバビリティバックエンド側でも見てみましょう。
トレースウォーターフォールで、POST /api/usersを実施しているスパンを選択すると、スパンタグとして先ほどユーザー登録時に指定した情報が確認できるようになったのではないでしょうか。
これでユーザーが入力したメールアドレスやユーザ名をトレースから確認できるようになりました。
例えば、オブザーバビリティバックエンド側に取得されたトレースを、このpayload.usernameの値に基づいて、トレース数やエラー数を集計したり、
特定のユーザ名を持つキーに、該当するトレースを抜き出したりすることができるようになります。
こんな感じで、分析や調査のキーとなる情報をスパンタグとして埋め込んでおくことで、調査すべき対象の処理にすぐに辿り着くことができるようになるわけです。
今回はスパン属性の追加を試みましたが、例えばアプリケーションコードの任意の場所で新たなスパンを作り、より処理の順序や流れを精緻に確認できるようにしていったり、ある特定の条件を満たした場合に強制的にエラー状態やExceptionをスパンに記録したりすることもできます。あるいは、あるアプリケーション処理に関する個別のメトリクスを生成することも可能です。要件に応じて、OpenTelemetry公式のドキュメントなどを参照しながら試してみてもらえればと思います(Pythonの場合の例)
計装を行う際は互換性に注意!
ここまで見ていただいた通り、自動計装・手動計装ともに、アプリケーションの言語別に構成をしていく必要があります。それはつまり、言語ごとに互換性や前提となるランタイムバージョンなどが定められていることを意味します。計装を行う際には、対象のアプリケーションについて互換性情報を確認しておくことが必要です。
例えば、Javaアプリケーションの場合、Java 8以上が原則で、AndroidやKotlinなどでは異なる前提が置かれています(Javaに関するポリシー)。他の言語でも同様のポリシーがありますので、事前に確認をしていきましょう。
自動計装にあたって、今回は環境変数を通じたエージェントの構成を行い、起動コマンドを変更しましたが、これらの環境変数や起動コマンドの管理は、それぞれアーキテクチャや利用しているフレームワークによっても異なるはずです。例えば、コンテナ化されたアプリケーションの場合は、環境変数をコンテナ作成時に使用するDockerfileに明記したり、起動時のコマンド引数にしたり、あるいは構成ファイル上に指定するようなこともできるはずです。あるいは、Javaアプリケーションにおいて、TomcatやJBoss(Wildfly)などのミドルウェアを使っており、そのミドルウェアごとの設定の管理が行われているケースもあるでしょう。
いずれの場合も、OpenTelemetryのエージェントがアプリケーションと共に起動できる状態で、かつ、必要な環境変数が与えられさえすればよいので、それぞれのアーキテクチャに合わせた形で設定を行うようにしてください(Javaアプリケーションのミドルウェアごとの設定例)。
この他、Kubernetes環境において、OpenTelemetry Operatorを利用することで自動計装を行う方法が公開されています。cert-managerが前提で、Podデプロイ時に自動計装用のエージェントをPodにInjectする方式を取ります。Kubernetes環境を利用中の場合は、こういった方法もご検討ください。
まとめ
いかがだったでしょうか。自動計装と手動計装のメリットと、その実装イメージを理解いただけたでしょうか。
今回のご紹介では、比較的シンプルな構成のアプリケーションにおいて計装を実施していったため、それほど難しくないように感じていただいたかもしれませんが、実際のアプリケーションやシステム環境においては必ずしも計装がスムーズに進まないケースもあります。次回はOpenTelemetryに基づく計装やその活用に関するトラブルシューティングについてご紹介しますので、引き続きお楽しみにしていただければと思います。