OpenTelemetry デモのトレースベーステスト

Blog posts are not updated after publication. This post is more than a year old, so its content may be outdated, and some links may be invalid. Cross-verify any information before relying on it.

Adnan RahićKen Hamric の協力を得て執筆。

OpenTelemetry デモは、Telescope Shop をシミュレートするシステムであり、異なるプログラミング言語で書かれた複数のマイクロサービスで構成されています。 各マイクロサービスは、この分散システムの特定の機能を担当しています。 デモの目的は、OpenTelemetry のツールと SDK をアプリケーションで使用して、監視結果のためのテレメトリーを取得し、さらには複数のサービスにまたがる問題の追跡にも活用できることを示すことです。

デモをメンテナンスする際の課題の1つは、エコシステムに新しい機能を追加しつつ、既存の機能とテレメトリーが意図通りに動作することを保証することです。 この問題を数か月前に考え始めた OpenTelemetry デモチームは、将来のシステム変更がマイクロサービスの結果やテレメトリーに意図しない影響を与えることを防ぐために議論を開始しました。 その結果、デモにトレースベースのテストが追加されました

この記事では、OpenTelemetry デモにトレースベースのテストがどのように追加されたかを説明します。 テストの構築中に直面した課題、その成果、そして OpenTelemetry コミュニティがデモのテストや機能追加をより高い信頼性のもとで行えるようになる方法について議論します。

トレースベーステストとは

トレースベーステストとは、システムに対してオペレーションをトリガーし、そのオペレーション中にシステムが生成したトレースを使ってシステム出力を検証することで、システムの動作を確認するテストの一種です。

この用語は、KubeCon North America 2018 での Ted Young による講演 Trace Driven Development: Unifying Testing and Observability で広まりました。

トレースベーステストを実行するには、システムに対してオペレーションを実行し、トレースを生成します。 以下の手順に従います。

  1. システムに対してオペレーションをトリガーし、その出力とオペレーションから生成されたトレース ID を収集します。
  2. システムがテレメトリーデータストアにトレース全体を報告するのを待ちます。
  3. オペレーション中にシステムが生成したトレースデータを収集します。 このデータには、タイミング情報、およびエラーや例外が含まれます。
  4. オペレーションの出力とトレースデータを期待される結果と照合して検証します。 これには、トレースデータを分析してシステムが期待通りに動作し、出力が正しかったことを確認する作業が含まれます。
  5. トレースデータが期待される結果と一致しない場合、テストは失敗します。 トレースデータを手元に持つことで、開発者は問題を調査し、システムまたはテストに必要な変更を加えることができます。
分散システムに対して実行されるトレースベーステストの例

この種のテストにより、分散システムの複数のコンポーネントを同時にテストし、それらが正しく連携して動作することを確認できます。 また、外部サービスからの障害や遅延レスポンスなど、実際の環境条件に対するシステムの動作をテストする方法も提供します。

OpenTelemetry デモのトレースベーステストの作成

OpenTelemetry デモでは、システムの変更が結果とテレメトリーの両方に意図しない影響を与えないことを検証するために、トレースベーステストを導入しました。 テストは主に、システムのメインワークフローに関与するサービスに焦点を当てました。 このワークフローには以下が含まれます。

  1. ユーザーがショップにアクセスする
  2. 商品を選択する
  3. 購入を決定する
  4. チェックアウトプロセスを完了する

デモに現在存在するテストに基づいて、2種類のテストを構成しました。

  • インテグレーションテスト
  • エンドツーエンドテスト

テストは 10 個のサービスに対する 26 個のトレースベーステストとして整理されています。 tracetesting ディレクトリのこれらのトレースベーステストは、AVA と Cypress から移植されたもので、オペレーションの結果とトレースの両方をテストします。

インテグレーションテスト

インテグレーションテストは AVA テストに基づいています。 これらのテストでは、システム内の各マイクロサービスのエンドポイントをトリガーし、そのレスポンスを検証し、結果として得られるオブザーバビリティのトレースが期待される動作と一致することを確認します。

1つの例として、通貨変換オペレーションが正しく返却されているかを確認する Currency Service に対して作成されたインテグレーションテストがあります。 以下は、このトレースベーステストの簡略化された YAML 定義です。

type: Test
spec:
  name: 'Currency: Convert'
  description: Convert a currency
  trigger:
    type: grpc
    grpc:
      protobufFile: { { protobuf file with CurrencyService definition } }
      address: { { currency service address } }
      method: oteldemo.CurrencyService.Convert
      request: |-
        {
          "from": {
            "currencyCode": "USD",
            "units": 330,
            "nanos": 750000000
          },
          "toCode": "CAD"
        }
  specs:
    - name: It converts from USD to CAD
      selector: span[name="CurrencyService/Convert" rpc.system="grpc"
        rpc.method="Convert" rpc.service="CurrencyService"]
      assertions:
        - attr:app.currency.conversion.from = "USD"
        - attr:app.currency.conversion.to = "CAD"
    - name: It has more nanos than expected
      selector: span[name="Test trigger"]
      assertions:
        - attr:response.body | json_path '$.nanos' >= 599380800

trigger セクションでは、どのオペレーションをトリガーするかを定義します。 この場合、メソッド oteldemo.CurrencyService.Convert と指定のペイロードを使った gRPC サービスへの呼び出しです。

その後、specs セクションでは、トレースとオペレーション結果に対してどのアサーションを行うかを定義します。

2種類のアサーションがあります。

  • 最初のアサーションは、CurrencyService が出力したトレースのスパンに対するものです。 スパンの属性 app.currency.conversion.fromapp.currency.conversion.to が正しい値を持つかどうかを確認することで、サービスが USD から CAD への変換オペレーションを受け取ったかを検証します。
  • 2番目のアサーションは、オペレーション出力を表すトレースのスパンに対して行われ、レスポンスボディの属性 nanos の値が 599380800 以下であるかを確認します。

エンドツーエンドテスト

エンドツーエンドテストは、Cypress を使ったフロントエンドテストに基づいています。 フロントエンドが使用する API を通じてサービスを呼び出し、サービス間のインタラクションが正しいかを確認します。 また、トレースがサービスを通じて正しく伝搬されているかも検証します。

これらのテストでは、デモの主要なユースケースに基づくシナリオを想定しました。 「ユーザーが商品を購入する」シナリオは、Front-end service の API に対して以下のオペレーションを実行します。

  • ショップに入ると、ユーザーには以下が表示されます。
    • ショップの商品の広告。
    • ユーザーに適した商品のレコメンデーション。
  • ユーザーが商品を閲覧します。
  • 商品をショッピングカートに追加します。
  • カートの内容が正しいか確認します。
  • 最後に、ショッピングカートのチェックアウト機能を使って注文を完了します。 これにより注文が確定され、ユーザーのクレジットカードに課金され、商品が発送され、ショッピングカートがクリアされます。

このテストは小さなテストの連続であるため、実行されるテストを定義するトランザクションを作成しました。

type: Transaction
spec:
  name: 'Frontend Service'
  description:
    Run all Frontend tests enabled in sequence, simulating a process of a user
    purchasing products on the Astronomy store
  steps:
    - ./01-see-ads.yaml
    - ./02-get-product-recommendation.yaml
    - ./03-browse-product.yaml
    - ./04-add-product-to-cart.yaml
    - ./05-view-cart.yaml
    - ./06-checking-out-cart.yaml

このテストシーケンスでは、ユーザーがチェックアウトを行う最後のステップが興味深いです。 これはオペレーションが複雑であるためです。 このオペレーションは、ほぼすべてのシステムサービスへの呼び出しを調整してトリガーしていることが、以下の Jaeger のスクリーンショットで確認できます。

checkout service formatted

このオペレーションでは、FrontendCheckoutServiceCartServiceProductCatalogServiceCurrencyService など、複数のサービスへの内部呼び出しを確認できます。

これはトレースベーステストの良いシナリオであり、出力が正しいこと、およびこのプロセスで呼び出されたサービスが正しく連携して動作していることを確認できます。 チェックアウト中にトリガーされる主要な機能を検証する、5つのアサーショングループを作成しました。

  • 「フロントエンドが正常に呼び出された」、テストトリガーの出力を確認します。
  • 「注文が確定された」CheckoutService が呼び出され、スパンが正しく出力されたかを確認します。
  • 「ユーザーに課金された」PaymentService が呼び出され、スパンが正しく出力されたかを確認します。
  • 「商品が発送された」ShippingService が呼び出され、スパンが正しく出力されたかを確認します。
  • 「カートが空になった」CartService が呼び出され、スパンが正しく出力されたかを確認します。

最終的なテスト YAML は以下の通りです。 チェックアウトオペレーションをトリガーし、これら5つのアサーショングループを検証します。

type: Test
spec:
  name: 'Frontend: Checking out shopping cart'
  description: Simulate user checking out shopping cart
  trigger:
    type: http
    httpRequest:
      url: http://{{frontend address}}/api/checkout
      method: POST
      headers:
        - key: Content-Type
          value: application/json
      body: |
        {
          "userId": "2491f868-88f1-4345-8836-d5d8511a9f83",
          "email": "someone@example.com",
          "address": {
            "streetAddress": "1600 Amphitheatre Parkway",
            "state": "CA",
            "country": "United States",
            "city": "Mountain View",
            "zipCode": "94043"
          },
          "userCurrency": "USD",
          "creditCard": {
            "creditCardCvv": 672,
            "creditCardExpirationMonth": 1,
            "creditCardExpirationYear": 2030,
            "creditCardNumber": "4432-8015-6152-0454"
          }
        }
  specs:
    - name: 'The frontend has been called with success'
      selector: span[name="Test trigger"]
      assertions:
        - attr:response.status = 200
    - selector:
        span[name="oteldemo.CheckoutService/PlaceOrder" rpc.system="grpc"
        rpc.method="PlaceOrder" rpc.service="oteldemo.CheckoutService"]
      name: 'The order was placed'
      assertions:
        - attr:app.user.id = "2491f868-88f1-4345-8836-d5d8511a9f83"
        - attr:app.order.items.count = 1
    - selector: span[name="oteldemo.PaymentService/Charge" rpc.system="grpc"
        rpc.method="Charge" rpc.service="oteldemo.PaymentService"]
      name: 'The user was charged'
      assertions:
        - attr:rpc.grpc.status_code  =  0
        - attr:selected_spans.count >= 1
    - selector: span[name="oteldemo.ShippingService/ShipOrder" rpc.system="grpc"
        rpc.method="ShipOrder" rpc.service="oteldemo.ShippingService"]
      name: 'The product was shipped'
      assertions:
        - attr:rpc.grpc.status_code = 0
        - attr:selected_spans.count >= 1
    - selector: span[name="oteldemo.CartService/EmptyCart" rpc.system="grpc"
        rpc.method="EmptyCart" rpc.service="oteldemo.CartService"]
      name: 'The cart was emptied'
      assertions:
        - attr:rpc.grpc.status_code = 0
        - attr:selected_spans.count >= 1

最後に、これらのテストを実行すると以下のレポートが得られます。 トランザクション内で実行された各テストファイルと、上記で説明した「チェックアウト」ステップが表示されます。

✔  Frontend Service (http://tracetest-server:11633/transaction/frontend-all/run/1)
  ✔  Frontend: See Ads (http://tracetest-server: 11633/test/frontend-see-adds/run/1/test)
    ✔  It called the frontend with success and got a valid redirectUrl for each ads
    ✔  It returns two ads
  ✔  Frontend: Get recommendations (http://tracetest-server: 11633/test/frontend-get-recommendation/run/1/test)
    ✔  It called the frontend with success
    ✔  It called ListRecommendations correctly and got 5 products
  ✔  Frontend: Browse products (http://tracetest-server:11633/test/frontend-browse-product/run/1/test)
    ✔  It called the frontend with success and got a product with valid attributes
    ✔  It queried the product catalog correctly for a specific product
  ✔  Frontend: Add product to the cart (http://tracetest-server:11633/test/frontend-add-product/run/1/test)
    ✔  It called the frontend with success
    ✔  It added an item correctly into the shopping cart
    ✔  It set the cart item correctly on the database
  ✔  Frontend: View cart (http://tracetest-server:11633/test/frontend-view-cart/run/1/test)
    ✔  It called the frontend with success
    ✔  It retrieved the cart items correctly
  ✔  Frontend: Checking out shopping cart (http://tracetest-server: 11633/test/frontend-checkout-shopping-cart/run/1/test)
    ✔  It called the frontend with success
    ✔  The order was placed
    ✔  The user was charged
    ✔  The product was shipped
    ✔  The cart was emptied

テストの実行と OpenTelemetry デモの評価

テストスイートが完成したら、デモで make run-tracetesting を実行してテストを実行します。 これにより、OpenTelemetry デモのすべてのサービスが評価されます。

テストの開発中に、テスト結果にいくつかの差異が見つかりました。 たとえば、Cypress テストにいくつかの軽微な修正が加えられ、バックエンド API でいくつかの動作が観察されました。 これらは後でテストおよび調査できます。 詳細はこのプルリクエストこのディスカッションで確認できます。

興味深い事例の1つは、EmailService の動作でした。 初めてテストを構築し、AVA テストで提供されたペイロードを使って直接呼び出したところ、サービスに対してトレースが生成され成功を示していましたが、Jaeger で確認すると HTTP 500 エラーが発生していました。

single-email-formatted.png

しかし、チェックアウトプロセスの一部として実行した場合は、この Jaeger スクリーンショットに示されるように、期待通りに実行されました。

email-under-checkout-formatted.png

何が起こったのでしょうか。 テレメトリーとコードを詳しく調べたところ、Email サービスはメールテンプレートの処理の性質上、Ruby で書かれており snake_case 標準を使用しているため、JSON で注文の詳細を pascalCase で送信するかわりに、

{
  "email": "google@example.com",
  "order": {
    "orderId": "505",
    "shippingCost": {
      "currencyCode": "USD"
    }
    // ...
  }
}

snake_case で渡す必要があることがわかりました。 Checkout サービスはこれを正しく行っています。

{
  "email": "google@example.com",
  "order": {
    "order_id": "505",
    "shipping_cost": {
      "currency_code": "USD"
    }
    // ...
  }
}

そうすることで、サービスへの呼び出しが成功し、以下に示すように正しく評価されます。

email-success-formatted.png

この種の事例は、他の実際のシナリオでも発生する可能性があるため興味深いです。 テストとテレメトリーデータの助けを借りて、問題を特定し解決することができました。 このテストの場合は、Checkout サービスと同じパターンを使用しない選択をしました。

まとめ

この記事では、システムへの変更がマイクロサービスの結果やテレメトリーに意図しない影響を与えないことを確認するために、OpenTelemetry デモにトレースベーステストがどのように追加されたかを議論しました。

これらのテストにより、OpenTelemetry コミュニティはデモに新しい機能を追加し、他のコンポーネントに意図しない副作用が発生していないかを簡単に検証でき、テレメトリーが正しく報告されていることを確認できます。

オープンソースのオブザーバビリティツールを構築するチームとして、OpenTelemetry コミュニティ全体に貢献する機会を大切にしています。 そのため、2か月前にこのイシューが作成されてすぐに対応を開始しました。