apollo-http-client 0.3.0

HTTP client for Apollo platform
Documentation
//! Integration tests for OTel span recording.
#![allow(unused_imports)]

#[macro_use]
mod common;

use std::net::SocketAddr;

use apollo_http_client::{HttpBody, HttpClient, HttpClientConfig};
use apollo_opentelemetry::metrics::Clock;
use apollo_opentelemetry_test::{TelemetryContext, assert_spans_snapshot};
use axum::Router;
use axum::routing::{get, post};
use bytes::Bytes;
use http_body_util::{BodyExt as _, Full};
use indoc::indoc;
use tower::ServiceExt as _;

use common::*;

// --- Spans --------------------------------------------------------------------

/// A successful GET emits an OTel Client span with the required HTTP semantic
/// convention attributes.
#[tokio::test]
async fn h1_records_client_span() {
    let ctx = integration_context();
    let addr = spawn_server(Router::new().route("/", get(|| async { "hello" }))).await;
    let req = http::Request::builder()
        .method(http::Method::GET)
        .uri(format!("http://{addr}/"))
        .body(empty_body())
        .unwrap();
    let _ = new_client(&default_config())
        .oneshot(req)
        .await
        .expect("ok");

    span_snapshot!(ctx, @r#"
    - name: GET
      span_kind: Client
      is_sampled: true
      attributes:
        http.request.method: GET
        http.response.status_code: "200"
        network.protocol.version: "1.1"
        server.address: 127.0.0.1
        server.port: "<port>"
        url.full: "http://127.0.0.1:<port>/"
    "#);
}

/// A 4xx response sets `error.type` on the span and marks the span status as Error.
#[tokio::test]
async fn h1_4xx_sets_span_error_status() {
    let ctx = integration_context();
    let addr =
        spawn_server(Router::new().route("/", get(|| async { http::StatusCode::NOT_FOUND }))).await;
    let req = http::Request::builder()
        .method(http::Method::GET)
        .uri(format!("http://{addr}/"))
        .body(empty_body())
        .unwrap();
    let _ = new_client(&default_config())
        .oneshot(req)
        .await
        .expect("ok");

    span_snapshot!(ctx, @r#"
    - name: GET
      span_kind: Client
      is_sampled: true
      attributes:
        error.type: "404"
        http.request.method: GET
        http.response.status_code: "404"
        network.protocol.version: "1.1"
        server.address: 127.0.0.1
        server.port: "<port>"
        url.full: "http://127.0.0.1:<port>/"
      status: Error
    "#);
}

/// Opt-in body size span attributes record the actual bytes sent and received.
#[tokio::test]
async fn h1_span_records_opt_in_body_sizes() {
    let ctx = integration_context();
    let addr =
        spawn_server(Router::new().route("/", post(|_body: axum::body::Bytes| async { "hello" })))
            .await;
    let config = parse_config(indoc! {"
        telemetry:
          spans:
            request_body_size: true
            response_body_size: true
    "});
    let req = http::Request::builder()
        .method(http::Method::POST)
        .uri(format!("http://{addr}/"))
        .body(bytes_body(Bytes::from(vec![0u8; 10])))
        .unwrap();
    let resp = new_client(&config).oneshot(req).await.expect("ok");
    resp.into_body().collect().await.unwrap();

    span_snapshot!(ctx, @r#"
    - name: POST
      span_kind: Client
      is_sampled: true
      attributes:
        http.request.body.size: "10"
        http.request.method: POST
        http.response.body.size: "5"
        http.response.status_code: "200"
        network.protocol.version: "1.1"
        server.address: 127.0.0.1
        server.port: "<port>"
        url.full: "http://127.0.0.1:<port>/"
    "#);
}

/// Request and response headers are captured as span attributes when configured.
#[tokio::test]
async fn h1_captures_request_and_response_headers() {
    let ctx = integration_context();
    let addr = spawn_server(Router::new().route(
        "/",
        get(|| async {
            let mut resp = axum::response::Response::new(axum::body::Body::empty());
            resp.headers_mut()
                .insert("x-trace-id", "trace-abc".parse().unwrap());
            resp
        }),
    ))
    .await;
    let config = parse_config(indoc! {"
        telemetry:
          spans:
            request_headers:
              - x-request-id
            response_headers:
              - x-trace-id
    "});
    let req = http::Request::builder()
        .method(http::Method::GET)
        .uri(format!("http://{addr}/"))
        .header("x-request-id", "req-123")
        .body(empty_body())
        .unwrap();
    let _ = new_client(&config).oneshot(req).await.expect("ok");

    span_snapshot!(ctx, @r#"
    - name: GET
      span_kind: Client
      is_sampled: true
      attributes:
        http.request.header.x-request-id: "[\"req-123\"]"
        http.request.method: GET
        http.response.header.x-trace-id: "[\"trace-abc\"]"
        http.response.status_code: "200"
        network.protocol.version: "1.1"
        server.address: 127.0.0.1
        server.port: "<port>"
        url.full: "http://127.0.0.1:<port>/"
    "#);
}