Skip to content

Latest commit

 

History

History
147 lines (114 loc) · 9.13 KB

File metadata and controls

147 lines (114 loc) · 9.13 KB

RequestBatcher cart and pricing observability sample

English | 简体中文

This Docker Compose sample models a small read-only pricing flow. A separate .NET 10 traffic-generator console app acts as an external client and calls the Cart Web API. Cart uses the named pricing-queries RequestBatcher pipeline to combine product lookups before calling the Pricing Web API. Pricing reads seeded products directly from PostgreSQL. Grafana OTel LGTM receives traces, metrics, and logs from the two Web APIs.

The sample uses the concise OpenTelemetry service names cart and pricing, so service selectors and trace views stay focused on the business services. Telemetry already stored with the previous requestbatcher-cart or requestbatcher-pricing names remains under those names until the backend retention period expires.

The sample is a local observability demonstration, not a benchmark or production deployment. It has two .NET 10 business projects, Cart and Pricing, plus the RequestBatcher.Observability.TrafficGenerator console project. Each traffic-generator request contains 2–10 products and emits no OpenTelemetry spans, so the Cart ASP.NET Core request span is the root visible in the trace. Product IDs 1 through 100 are seeded by PostgreSQL's initialization script.

Start

Run these commands from the repository root:

# Build images, start services in dependency order, and wait for health checks.
docker compose -f samples/RequestBatcher.Observability/compose.yaml up --build -d --wait
# Inspect the running services and published ports.
docker compose -f samples/RequestBatcher.Observability/compose.yaml ps

The Compose build uses the repository root as its context. up --build -d --wait builds the application images, starts the stack in dependency order, and waits for health checks. The default loopback endpoints are:

Service URL Purpose
Cart http://127.0.0.1:5081 Quote API and health endpoint
Pricing http://127.0.0.1:5082 Product price API and health endpoint
Grafana http://127.0.0.1:3000 Traces, metrics, logs, and dashboard
OTLP/gRPC http://127.0.0.1:4317 Optional local OTLP receiver
OTLP/HTTP http://127.0.0.1:4318 OTLP receiver used by the APIs

Override CART_PORT, PRICING_PORT, GRAFANA_PORT, OTLP_GRPC_PORT, or OTLP_HTTP_PORT when a default is in use. The traffic generator defaults to 32 workers and a 250 ms delay; override TRAFFIC_CONCURRENCY and TRAFFIC_DELAY_SECONDS when starting the stack.

Docker runs the separate CheckHealth.cs application to probe each API's /health route before starting dependent services. This checks HTTP availability, not database readiness. Business endpoints live in CartEndpoints.cs and PricingEndpoints.cs; the Program.cs files compose and start their hosts.

The repository's .NET 10 SDK can also run the health-check file application directly. The traffic generator is a regular project so Rider can build and run it:

dotnet run --project samples/RequestBatcher.Observability/TrafficGenerator/RequestBatcher.Observability.TrafficGenerator.csproj -- --help
dotnet run --project samples/RequestBatcher.Observability/TrafficGenerator/RequestBatcher.Observability.TrafficGenerator.csproj
dotnet run --file samples/RequestBatcher.Observability/CheckHealth.cs -- http://localhost:5081/health

Check both APIs and submit a manual quote:

curl --fail http://127.0.0.1:5081/health
curl --fail http://127.0.0.1:5082/health
curl --fail --request POST http://127.0.0.1:5081/cart/quote \
  --header 'Content-Type: application/json' \
  --data '[{"productId":1,"quantity":2},{"productId":2,"quantity":1}]'
curl --fail --request POST http://127.0.0.1:5081/cart/quote/ordinary \
  --header 'Content-Type: application/json' \
  --data '[{"productId":1,"quantity":2},{"productId":2,"quantity":1}]'

The quote response contains one line per requested product and the calculated total. Cart validates 2 to 10 entries, positive product IDs, and quantities from 1 through 100. Missing products return 404; invalid input returns 400. The original /cart/quote route uses the pricing-queries delegate pipeline; /cart/quote/ordinary uses the pricing-queries-ordinary handler-type pipeline with the same Pricing lookup behavior. Pricing also supports direct inspection and explicit grouped reads:

curl --fail http://127.0.0.1:5082/prices/1
curl --fail 'http://127.0.0.1:5082/prices?ids=1,2,3,64,100'

Observability

Open the RequestBatcher observability dashboard and sign in with admin / admin if prompted. The HTTP panels show request rate and duration; the RequestBatcher panels show submitted requests, batch size, and batch duration. The logs and trace panels show the two services' logs and Tempo trace families for RequestBatcher.Execute and RequestBatcher.Handle.

The visible trace starts with the Cart ASP.NET Core request span. Each Cart request creates one RequestBatcher.Execute span named for its pipeline (pricing-queries or pricing-queries-ordinary) for its complete 2–10 ID sequence. The shared Cart batch handler receives up to 20 product requests, mixes requests from concurrent Cart calls, deduplicates their IDs, and makes one HTTP call to Pricing for the group. Pricing's ASP.NET Core span contains the Npgsql database span created by Npgsql's official OpenTelemetry integration. The Cart batch spans are independent Consumer roots. Each Handle span links to the contributing caller Execute contexts and carries requestbatcher.name=pricing-queries. A single Cart trace has one Execute span, but an explicit 2–10 item submission can be split across multiple Handle spans; a batch can also mix product requests from concurrent Cart traces. requestbatcher.batch.size records product requests in that batch.

To follow one call into a batch, open the Cart trace, copy the Execute span's trace ID, and search Tempo with:

{ link:traceID = "CALL_TRACE_ID" && span:name =~ "RequestBatcher[.]Handle( .*)?" }

The pinned Tempo build stores this query field without leading zeroes. Remove leading 0 characters from a copied trace ID before substituting it above. Select a time range that includes the original call. A batch can contain fewer than 20 submissions because RequestBatcher does not wait to fill BatchSize.

Open the returned batch span to inspect the Cart-to-Pricing HTTP child and the Pricing database span. Activity links are causal references rather than shared parent edges, so they do not imply one shared trace tree. Each Handle span links to every contributing Execute span, and each Execute span links to every Handle span that handled its requests. See Grafana's TraceQL link query documentation for the link:traceID and link:spanID selectors.

The following example uses the default /cart/quote endpoint.

For direct navigation, open a Handle trace from the dashboard's Handler execution · Handle table, click the RequestBatcher.Handle pricing-queries span row, and expand References in the span details to see its Execute callers. Click an Execute span in the Cart calls · Execute table and expand References to see the Handle spans that processed it. Grafana groups parent references and span links in this list: the cart POST /cart/quote entry is Execute's HTTP parent in the same trace, while handler links target independent Handle traces. The parent entry is not an extra ActivityLink. To find the Execute spans from a batch, copy its trace and span IDs and query Tempo with:

{ link:traceID = "BATCH_TRACE_ID" && link:spanID = "BATCH_SPAN_ID" && span:name = "RequestBatcher.Execute pricing-queries" }

The pinned Tempo build stores this query field without leading zeroes; remove leading 0 characters from copied IDs before substituting them above. Select a time range that includes the original batch. The TraceQL query above is useful when the backend does not expose a clickable link in the span details.

Cart and pricing business flow

RequestBatcher observability flow

The sample design documents the current flow and compatibility contract. The sample source shows the two Web APIs, the traffic generator, and their telemetry setup.

For end-to-end validation, see the integration test guide (中文). Running the full integration-test project requires Docker; its Testcontainers fixture creates an isolated environment and cleans it up after validation. The live test checks one Cart Execute per HTTP request, cross-trace batch links, Pricing/database parent-child spans, and metrics. To run the in-process tests without Docker, use the TracingStressTests and MetricsAdviceTests filter shown in the guide.

RequestBatcher is in-process and does not recover pending requests after a process failure.