Trace-based Testing the OpenTelemetry Demo

博客文章在发布后不会更新。这篇文章已经发布一年多了,其内容可能已过时,部分链接可能无效。在依赖任何信息之前,请务必核实。

参与贡献者:Adnan RahićKen Hamric

OpenTelemetry Demo 是一个模拟望远镜商店的系统,由多种不同语言编写的微服务组成,每个微服务负责该分布式系统的特定功能。其目的是演示如何使用应用程序中的 OpenTelemetry 工具和 SDK 来获取遥测数据,以监控结果,甚至 追踪跨多个服务的问题

维护该演示版的一个挑战是为生态系统添加新功能,并确保当前功能和遥测数据按预期工作。几个月前,OpenTelemetry Demo 团队在思考这个问题时,发起了一场讨论,以防止未来系统更改对微服务结果及其遥测数据产生不利影响。这促成了向演示版添加基于追踪的测试

本文介绍了如何将基于追踪的测试添加到 OpenTelemetry Demo。它讨论了我们在构建过程中遇到的挑战、结果以及它如何帮助 OpenTelemetry 社区更有信心地测试演示版和添加功能。

什么是基于追踪的测试?

基于追踪的测试是一种通过触发对系统的操作,并检查系统在调用期间发出的追踪来验证系统行为的测试类型。

这个术语由 Ted Young 在 2018 年 KubeCon 北美大会的演讲《Trace Driven Development: Unifying Testing and Observability》中推广。

要执行基于追踪的测试,我们需要执行一个对系统的操作,该操作会生成一个追踪,遵循以下步骤:

  1. 触发对系统的操作,并收集其输出和操作生成的追踪 ID。
  2. 等待系统将整个追踪报告给遥测数据存储;
  3. 收集系统在操作期间生成的追踪数据。这些数据应包括时间信息,以及遇到的任何错误或异常。
  4. 通过将操作输出和追踪数据与预期结果进行比较来验证它们。这包括分析追踪数据,以确保系统表现符合预期,并且输出是正确的。
  5. 如果追踪数据与预期结果不匹配,则测试应失败。获得追踪数据后,开发人员可以调查问题并对系统或测试进行必要的更改。
Example of Trace-based test running against a distributed system

这类测试允许同时测试分布式系统中的多个组件,确保它们协同工作。它还提供了一种测试系统在真实条件下(如外部服务的故障或响应缓慢)的行为的方式。

为 OpenTelemetry Demo 创建基于追踪的测试

在 OpenTelemetry Demo 中,我们包含了基于追踪的测试,以验证系统更改不会对结果和遥测数据产生不利影响。我们的测试主要集中在系统中主要工作流程所涉及的服务,包括:

  1. 用户浏览商店
  2. 选择产品
  3. 决定购买
  4. 完成结账流程

我们根据演示版中现有的测试构建了两种类型的测试:

  • 集成测试
  • 端到端测试

这些测试被组织在tracetesting 目录中,共 26 个基于追踪的测试,覆盖 10 个服务。这些测试是从 AVA 和 Cypress 移植过来的,并测试操作结果和追踪数据。

集成测试

集成测试基于AVA测试。在这些测试中,我们触发系统中每个微服务的端点,验证它们的响应,并确保生成的观测追踪符合预期行为。

一个例子是对货币服务进行的集成测试,用于检查货币转换操作是否正确返回。下面是一个基于追踪的测试的简化 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` 部分,我们定义了要触发的操作。在这种情况下,调用 gRPC 服务,方法是 `oteldemo.CurrencyService.Convert`,并提供给定的负载。

之后,在 `specs` 部分,我们定义了要针对追踪和操作结果进行的断言。

我们可以看到两种类型的断言:

  • 第一个断言是针对 `CurrencyService` 发出的追踪跨度。它通过检查跨度属性 `app.currency.conversion.from` 和 `app.currency.conversion.to` 是否具有正确的值,来检查服务是否收到了从 USD 到 CAD 的转换操作;
  • 第二个断言是针对代表操作输出的追踪跨度,我们在其中检查响应体是否具有一个值小于或等于 `599380800` 的 `nanos` 属性。

端到端测试

端到端测试基于使用Cypress的前端测试。我们通过前端使用的 API 调用服务,并检查它们之间的服务交互是否正确。我们还验证追踪是否正确地在服务之间传播。

对于这些测试,我们考虑了一个基于演示版主要用例的场景:“用户购买产品”,该场景是针对前端服务 API 执行的,这些 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 等。

这是一个很好的基于追踪的测试场景,我们可以检查输出是否正确,以及在此过程中调用的服务是否能正确协同工作。我们开发了五组断言,检查结账过程中触发的主要功能:

  • “前端已成功调用”,检查测试触发器的输出;
  • “订单已下单”,检查 CheckoutService 是否被调用并正确发出跨度;
  • “用户已付款”,检查 PaymentService 是否被调用并正确发出跨度;
  • “产品已发货”,检查 ShippingService 是否被调用并正确发出跨度;
  • “购物车已清空”,检查 CartService 是否被调用并正确发出跨度。

最终结果是以下的测试 YAML,它触发了 Checkout 操作并验证了这五组断言:

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

最后,当我们运行这些测试时,可以看到以下的报告,显示了事务中运行的每个测试文件,以及上面描述的“Checkout”步骤:

✔  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 Demo

完成测试套件后,通过在演示版中执行 `make run-tracetesting` 来运行它。这将评估 OpenTelemetry Demo 中的所有服务。

在测试开发过程中,我们注意到测试结果存在一些差异。例如,Cypress 测试做了一些小的修复,并且观察到后端 API 的一些行为,这些行为可以在以后进行测试和调查。您可以在此拉取请求此讨论中找到详细信息。

一个有趣的案例是EmailService的行为。首次构建测试并直接使用 AVA 测试提供的负载调用它时,服务生成了表示成功的追踪,但带有 HTTP 500 错误,如 Jaeger 中所示。

single-email-formatted.png

然而,在结账过程中作为一部分运行它时,它如 Jaeger 截图中所示,按预期执行:

email-under-checkout-formatted.png

可能发生了什么?通过深入查看遥测数据和代码,我们发现由于 Email 服务处理电子邮件模板的性质,该服务是用 Ruby 编写的,遵循 `snake_case` 标准,而不是发送 `pascalCase` 的订单详细信息作为 `JSON`。

{
  "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 Demo 中,以帮助确保系统更改不会对微服务结果及其遥测数据产生不利影响。

通过这些测试,OpenTelemetry 社区可以为演示版添加新功能,有一个简单的方法来验证其他组件是否没有受到任何不利的副作用,并且仍然正确地报告遥测数据。

作为一个构建开源可观测性工具的团队,我们重视为整个 OpenTelemetry 社区做出贡献的机会。这就是为什么我们在两个月前该问题一经提出就立即采取行动的原因。

最后修改于 2025 年 5 月 22 日:[chore] Accessible links 4 (#6052) (1609d60e)