OpenTelemetry演示的基于跟踪的测试
由Adnan Rahić和Ken Hamric共同贡献。
OpenTelemetry演示是一个模拟望远镜商店的系统,由多个用不同语言编写的微服务组成,每个微服务都处理这个分布式系统的特定能力。其目的是演示如何在应用程序中使用OpenTelemetry工具和SDK,以获取用于监控结果甚至跨多个服务追踪问题的遥测数据。
维护演示时的一个挑战是向生态系统添加新功能并确保当前功能和遥测数据正常工作。几个月前,在思考这个问题时,OpenTelemetry演示团队开始了一次讨论,以防止未来的系统更改对微服务结果和遥测数据产生意外后果。这导致向演示中添加了基于跟踪的测试。
本文描述了如何向OpenTelemetry演示添加基于跟踪的测试。它讨论了我们在构建过程中面临的挑战、结果以及如何帮助OpenTelemetry社区以更大的信心测试演示和添加功能。
什么是基于跟踪的测试?
基于跟踪的测试是一种通过触发对系统的操作并使用系统在此调用期间生成的跟踪来验证系统行为的测试类型。
这个术语由Ted Young在2018年KubeCon North America的演讲《跟踪驱动开发:统一测试和可观察性》中推广。
要执行基于跟踪的测试,我们执行一个针对系统的操作,生成一个跟踪,并按照以下步骤进行:
- 对系统执行一个操作,并收集操作的输出和由操作生成的跟踪ID。
- 等待系统报告整个跟踪到遥测数据存储区。
- 收集系统在操作期间生成的跟踪数据。此数据应包括时序信息以及在操作过程中遇到的任何错误或异常。
- 通过将其与预期结果进行比较,验证操作输出和跟踪数据。这涉及分析跟踪数据,以确保系统的行为符合预期并且输出是正确的。
- 如果跟踪数据与预期结果不符,则测试应该失败。开发人员可以凭借跟踪数据来调查问题并对系统或测试进行必要的更改。
这些类型的测试允许同时测试分布式系统中的多个组件,确保它们正确协同工作。它还提供了一种测试系统在面对真实世界情况(如来自外部服务的故障或响应时间过长)时的行为的方法。
为OpenTelemetry演示创建基于跟踪的测试
在OpenTelemetry演示中,我们包含了基于跟踪的测试,以验证系统更改不会导致结果和遥测数据产生意外的结果。我们的测试主要集中在系统主要工作流程中涉及的服务上,包括:
- 用户访问商店
- 选择一个产品
- 决定购买
- 完成结账流程
我们根据演示中当前存在的测试结构化了两种类型的测试:
- 集成测试
- 端到端测试
这些测试组织成了10个服务的26个基于跟踪的测试,可以在这里找到。tracetesting
目录中的这些基于跟踪的测试是从AVA和Cypress移植过来的,既测试操作结果,又测试跟踪。
集成测试
集成测试基于AVA测试。在这些测试中,我们触发系统中每个微服务的端点,验证其响应,并确保生成的可观测性跟踪与预期的行为相匹配。
一个示例是针对货币服务进行的integration test,以检查货币转换操作是否返回正确。以下是这个基于跟踪测试的简化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
部分,我们定义了要针对跟踪和操作结果进行的断言。
我们可以看到两种类型的断言:
- 第一个断言针对跟踪中的一个span,它是
CurrencyService
发出的。通过检查跨度属性app.currency.conversion.from
和app.currency.conversion.to
是否具有正确的值,它检查服务是否接收到了来自USD到CAD的转换操作。 - 第二个断言对一个表示操作输出的跟踪span进行断言,在这里我们检查响应体是否具有小于等于
599380800
的nanos
属性值。
端到端测试
端到端测试基于使用Cypress的前端测试。我们通过前端使用的API调用服务,并检查它们之间的服务交互是否正确。我们还验证跟踪是否被正确地传播到各个服务。
对于这些测试,我们考虑了基于演示的主要用例的场景:“一个用户购买一个产品”,该场景通过针对前端服务的API执行以下操作来实现:
- 在进入商店时,用户会看到:
- 商店产品的广告。
- 适合他们的产品推荐。
- 用户选择浏览某个产品。
- 将其添加到购物车。
- 检查购物车以确保一切都正确。
- 最后,使用购物车的结账功能完成订单,它将下订单、扣款用户的信用卡、发货产品并清空购物车。
由于这个测试是一系列较小测试的顺序,我们创建了一个定义将运行的测试的a交易:
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
在这一系列测试中,最后一步“checkout”非常有趣,因为它由于操作的复杂性而被触发。它协调并触发几乎所有系统服务的调用,就像我们在这个操作的Jaeger跟踪屏幕截图中看到的那样:
在这个操作中,我们可以看到对多个服务的内部调用,例如前端、结账服务、购物车服务、产品目录服务、货币服务等。
这是一个非常适合基于跟踪的测试的场景,我们可以检查输出是否正确,以及在此过程中调用的服务是否正常工作。我们制定了五组断言,检查结账过程中触发的主要功能:
- “前端已成功调用”,检查测试的触发输出。
- “订单已下达”,检查是否调用了结账服务并正确发出跨度。
- “用户已被扣款”,检查是否调用了支付服务并正确发出跨度。
- “产品已发货”,检查是否调用了运输服务并正确发出跨度。
- “购物车已清空”,检查是否调用了购物车服务并正确发出跨度。
最终的测试结果如下,显示了在事务中运行的每个测试文件,以及上述“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演示
有了完整的测试套件,通过在演示中执行make run-tracetesting
来运行它。这将评估OpenTelemetry演示中的所有服务。
在开发测试过程中,我们注意到测试结果有一些差异。例如,对Cypress测试进行了一些小的修复,并观察到了后端API的一些行为,可以在以后测试和调查。您可以在此拉取请求和此讨论中找到详细信息。
有趣的案例是电子邮件服务的行为。在首次建立测试并使用AVA测试提供的负载直接调用它时,Jaeger生成了表示成功的服务跟踪,但伴随着一个HTTP 500
错误,如下所示:
然而,作为结账流程的一部分运行它时,它按预期执行,如下Jaeger截图所示:
到底发生了什么?通过更深入地查看遥测数据和代码,我们发现由于使用以Ruby编写的Email服务处理电子邮件模板的特性,它使用了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"
}
// ...
}
}
通过这样做,我们对服务进行了成功调用,并且它正确地进行评估,如下所示:
这种情况非常有趣,因为它在其他真实场景中也可能发生,并且借助于测试和遥测数据的帮助,我们能够准确定位并解决它。在this test的情况下,我们选择不使用与Checkout服务相同的模式。
结论
本文讨论了如何向OpenTelemetry演示添加基于跟踪的测试,以确保对系统的更改不会对微服务结果和遥测数据产生意外的结果。
通过这些测试,OpenTelemetry社区可以向演示添加新功能,并轻松验证其他组件没有遭受任何意外副作用,并且仍然正确报告遥测数据。
作为构建开源可观测性工具的团队,我们重视为整个OpenTelemetry社区做出贡献的机会。因此,我们在两个月前发现问题后立即采取了行动。