#![allow(unused_imports)]
#[macro_use]
mod common;
use std::net::SocketAddr;
use std::time::Duration;
use apollo_http_client::{HttpBody, HttpClient, HttpClientConfig};
use apollo_opentelemetry::metrics::Clock;
use apollo_opentelemetry_test::{TelemetryContext, assert_metric, assert_metrics_snapshot};
use axum::Router;
use axum::routing::get;
use bytes::Bytes;
use http_body_util::{BodyExt as _, Full};
use hyper_util::rt::{TokioExecutor, TokioIo};
use indoc::indoc;
use opentelemetry_sdk::metrics::{Aggregation, Instrument, StreamBuilder};
use tower::ServiceExt as _;
use common::*;
async fn spawn_h2c_server(router: Router) -> SocketAddr {
use hyper::server::conn::http2;
use hyper_util::service::TowerToHyperService;
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
tokio::spawn(async move {
loop {
let Ok((stream, _)) = listener.accept().await else {
break;
};
let io = TokioIo::new(stream);
let svc = TowerToHyperService::new(router.clone());
tokio::spawn(async move {
http2::Builder::new(TokioExecutor::new())
.serve_connection(io, svc)
.await
.ok();
});
}
});
addr
}
fn h2_config() -> HttpClientConfig {
parse_config("protocol: http2")
}
fn h2_connections_context() -> TelemetryContext {
TelemetryContext::builder()
.with_view(|instrument: &Instrument| {
if instrument.name() != "http.client.open_connections" {
Some(
StreamBuilder::default()
.with_aggregation(Aggregation::Drop)
.build()
.unwrap(),
)
} else {
None
}
})
.build()
}
#[tokio::test]
async fn h2_connection_refused_records_error() {
let ctx = TelemetryContext::new();
let config = parse_config("protocol: http2\nconnect_timeout: 1s");
let req = http::Request::builder()
.method(http::Method::GET)
.uri("http://127.0.0.1:1/")
.body(empty_body())
.unwrap();
let _ = new_client(&config).oneshot(req).await;
assert_metrics_snapshot!(ctx, @r#"
- name: http.client.active_requests
description: Number of HTTP requests currently in flight
unit: "{request}"
data:
type: Sum
data_points:
- attributes:
http.request.method: GET
server.address: 127.0.0.1
server.port: "1"
value: 0
is_monotonic: false
temporality: Cumulative
- name: http.client.request.duration
description: Duration of HTTP client requests
unit: s
data:
type: Histogram
data_points:
- attributes:
error.type: _OTHER
http.request.method: GET
network.protocol.version: "2"
server.address: 127.0.0.1
server.port: "1"
count: 1
sum: 0
min: 0
max: 0
bounds:
- 0.005
- 0.01
- 0.025
- 0.05
- 0.075
- 0.1
- 0.25
- 0.5
- 0.75
- 1
- 2.5
- 5
- 7.5
- 10
bucket_counts:
- 1
- 0
- 0
- 0
- 0
- 0
- 0
- 0
- 0
- 0
- 0
- 0
- 0
- 0
- 0
temporality: Cumulative
"#);
}
#[tokio::test]
async fn h2_server_closes_before_handshake() {
let ctx = integration_context();
let addr = spawn_drop_server().await;
let req = http::Request::builder()
.method(http::Method::GET)
.uri(format!("http://{addr}/"))
.body(empty_body())
.unwrap();
let err = new_client(&h2_config())
.oneshot(req)
.await
.expect_err("should fail");
assert!(matches!(
err,
apollo_http_client::HttpClientError::Request { .. }
));
snapshot!(ctx, @r#"
- name: http.client.active_requests
description: Number of HTTP requests currently in flight
unit: "{request}"
data:
type: Sum
data_points:
- attributes:
http.request.method: GET
server.address: 127.0.0.1
server.port: "<port>"
value: 0
is_monotonic: false
temporality: Cumulative
- name: http.client.open_connections
description: Number of open connections in the HTTP client pool
unit: "{connection}"
data:
type: Sum
data_points:
- attributes:
http.connection.state: active
network.protocol.version: "2"
server.address: 127.0.0.1
server.port: "<port>"
value: 0
- attributes:
http.connection.state: idle
network.protocol.version: "2"
server.address: 127.0.0.1
server.port: "<port>"
value: 0
is_monotonic: false
temporality: Cumulative
- name: http.client.request.duration
description: Duration of HTTP client requests
unit: s
data:
type: Histogram
data_points:
- attributes:
error.type: _OTHER
http.request.method: GET
network.protocol.version: "2"
server.address: 127.0.0.1
server.port: "<port>"
count: 1
sum: 0
min: 0
max: 0
bounds:
- 0.005
- 0.01
- 0.025
- 0.05
- 0.075
- 0.1
- 0.25
- 0.5
- 0.75
- 1
- 2.5
- 5
- 7.5
- 10
bucket_counts:
- 1
- 0
- 0
- 0
- 0
- 0
- 0
- 0
- 0
- 0
- 0
- 0
- 0
- 0
- 0
temporality: Cumulative
"#);
}
#[tokio::test]
async fn h2_server_closes_connection_during_request() {
use hyper::server::conn::http2;
use hyper_util::service::TowerToHyperService;
let ctx = integration_context();
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
tokio::spawn(async move {
let Ok((stream, _)) = listener.accept().await else {
return;
};
let io = TokioIo::new(stream);
let svc = TowerToHyperService::new(Router::new().route(
"/",
get(|| async {
tokio::time::sleep(Duration::MAX).await;
""
}),
));
let conn = http2::Builder::new(TokioExecutor::new()).serve_connection(io, svc);
tokio::pin!(conn);
let _ = tokio::time::timeout(Duration::from_millis(50), &mut conn).await;
});
let req = http::Request::builder()
.method(http::Method::GET)
.uri(format!("http://{addr}/"))
.body(empty_body())
.unwrap();
let err = new_client(&h2_config())
.oneshot(req)
.await
.expect_err("should fail");
assert!(matches!(
err,
apollo_http_client::HttpClientError::Request { .. }
));
snapshot!(ctx, @r#"
- name: http.client.active_requests
description: Number of HTTP requests currently in flight
unit: "{request}"
data:
type: Sum
data_points:
- attributes:
http.request.method: GET
server.address: 127.0.0.1
server.port: "<port>"
value: 0
is_monotonic: false
temporality: Cumulative
- name: http.client.open_connections
description: Number of open connections in the HTTP client pool
unit: "{connection}"
data:
type: Sum
data_points:
- attributes:
http.connection.state: active
network.protocol.version: "2"
server.address: 127.0.0.1
server.port: "<port>"
value: 0
- attributes:
http.connection.state: idle
network.protocol.version: "2"
server.address: 127.0.0.1
server.port: "<port>"
value: 0
is_monotonic: false
temporality: Cumulative
- name: http.client.request.duration
description: Duration of HTTP client requests
unit: s
data:
type: Histogram
data_points:
- attributes:
error.type: _OTHER
http.request.method: GET
network.protocol.version: "2"
server.address: 127.0.0.1
server.port: "<port>"
count: 1
sum: 0
min: 0
max: 0
bounds:
- 0.005
- 0.01
- 0.025
- 0.05
- 0.075
- 0.1
- 0.25
- 0.5
- 0.75
- 1
- 2.5
- 5
- 7.5
- 10
bucket_counts:
- 1
- 0
- 0
- 0
- 0
- 0
- 0
- 0
- 0
- 0
- 0
- 0
- 0
- 0
- 0
temporality: Cumulative
"#);
}
#[tokio::test]
async fn h2_successful_get_records_all_metrics() {
let ctx = integration_context();
let addr = spawn_h2c_server(Router::new().route("/", get(|| async { "hello" }))).await;
let config = parse_config(indoc! {"
protocol: http2
telemetry:
metrics:
request_body_size: true
response_body_size: true
url_scheme: true
"});
let client = new_client(&config);
let req = http::Request::builder()
.method(http::Method::GET)
.uri(format!("http://{addr}/"))
.body(empty_body())
.unwrap();
let resp = client.clone().oneshot(req).await.expect("request ok");
resp.into_body().collect().await.unwrap();
drop(client);
snapshot!(ctx, @r#"
- name: http.client.active_requests
description: Number of HTTP requests currently in flight
unit: "{request}"
data:
type: Sum
data_points:
- attributes:
http.request.method: GET
server.address: 127.0.0.1
server.port: "<port>"
url.scheme: http
value: 0
is_monotonic: false
temporality: Cumulative
- name: http.client.open_connections
description: Number of open connections in the HTTP client pool
unit: "{connection}"
data:
type: Sum
data_points:
- attributes:
http.connection.state: active
network.protocol.version: "2"
server.address: 127.0.0.1
server.port: "<port>"
url.scheme: http
value: 0
- attributes:
http.connection.state: idle
network.protocol.version: "2"
server.address: 127.0.0.1
server.port: "<port>"
url.scheme: http
value: 0
is_monotonic: false
temporality: Cumulative
- name: http.client.request.body.size
description: Size of HTTP request bodies sent by the client
unit: By
data:
type: Histogram
data_points:
- attributes:
http.request.method: GET
http.response.status_code: "200"
network.protocol.version: "2"
server.address: 127.0.0.1
server.port: "<port>"
url.scheme: http
count: 1
sum: 0
min: 0
max: 0
bounds:
- 0
- 1024
- 4096
- 16384
- 65536
- 262144
- 1048576
- 4194304
- 16777216
bucket_counts:
- 1
- 0
- 0
- 0
- 0
- 0
- 0
- 0
- 0
- 0
temporality: Cumulative
- name: http.client.request.duration
description: Duration of HTTP client requests
unit: s
data:
type: Histogram
data_points:
- attributes:
http.request.method: GET
http.response.status_code: "200"
network.protocol.version: "2"
server.address: 127.0.0.1
server.port: "<port>"
url.scheme: http
count: 1
sum: 0
min: 0
max: 0
bounds:
- 0.005
- 0.01
- 0.025
- 0.05
- 0.075
- 0.1
- 0.25
- 0.5
- 0.75
- 1
- 2.5
- 5
- 7.5
- 10
bucket_counts:
- 1
- 0
- 0
- 0
- 0
- 0
- 0
- 0
- 0
- 0
- 0
- 0
- 0
- 0
- 0
temporality: Cumulative
- name: http.client.response.body.size
description: Size of HTTP response bodies received by the client
unit: By
data:
type: Histogram
data_points:
- attributes:
http.request.method: GET
http.response.status_code: "200"
network.protocol.version: "2"
server.address: 127.0.0.1
server.port: "<port>"
url.scheme: http
count: 1
sum: 5
min: 5
max: 5
bounds:
- 0
- 1024
- 4096
- 16384
- 65536
- 262144
- 1048576
- 4194304
- 16777216
bucket_counts:
- 0
- 1
- 0
- 0
- 0
- 0
- 0
- 0
- 0
- 0
temporality: Cumulative
"#);
}
#[tokio::test]
async fn h2_4xx_response_records_status_and_error_type() {
let ctx = integration_context();
let addr =
spawn_h2c_server(Router::new().route("/", get(|| async { http::StatusCode::NOT_FOUND })))
.await;
let client = new_client(&h2_config());
let req = http::Request::builder()
.method(http::Method::GET)
.uri(format!("http://{addr}/"))
.body(empty_body())
.unwrap();
let resp = client.clone().oneshot(req).await.expect("request ok");
assert_eq!(resp.status(), http::StatusCode::NOT_FOUND);
drop(resp);
drop(client);
snapshot!(ctx, @r#"
- name: http.client.active_requests
description: Number of HTTP requests currently in flight
unit: "{request}"
data:
type: Sum
data_points:
- attributes:
http.request.method: GET
server.address: 127.0.0.1
server.port: "<port>"
value: 0
is_monotonic: false
temporality: Cumulative
- name: http.client.open_connections
description: Number of open connections in the HTTP client pool
unit: "{connection}"
data:
type: Sum
data_points:
- attributes:
http.connection.state: active
network.protocol.version: "2"
server.address: 127.0.0.1
server.port: "<port>"
value: 0
- attributes:
http.connection.state: idle
network.protocol.version: "2"
server.address: 127.0.0.1
server.port: "<port>"
value: 0
is_monotonic: false
temporality: Cumulative
- name: http.client.request.duration
description: Duration of HTTP client requests
unit: s
data:
type: Histogram
data_points:
- attributes:
error.type: "404"
http.request.method: GET
http.response.status_code: "404"
network.protocol.version: "2"
server.address: 127.0.0.1
server.port: "<port>"
count: 1
sum: 0
min: 0
max: 0
bounds:
- 0.005
- 0.01
- 0.025
- 0.05
- 0.075
- 0.1
- 0.25
- 0.5
- 0.75
- 1
- 2.5
- 5
- 7.5
- 10
bucket_counts:
- 1
- 0
- 0
- 0
- 0
- 0
- 0
- 0
- 0
- 0
- 0
- 0
- 0
- 0
- 0
temporality: Cumulative
"#);
}
#[tokio::test]
async fn h2_connection_stays_idle_between_requests() {
let ctx = h2_connections_context();
let addr = spawn_h2c_server(Router::new().route("/", get(|| async { "ok" }))).await;
let client = new_client(&h2_config());
for _ in 0..2 {
let req = http::Request::builder()
.method(http::Method::GET)
.uri(format!("http://{addr}/"))
.body(empty_body())
.unwrap();
let resp = client.clone().oneshot(req).await.expect("request ok");
resp.into_body().collect().await.unwrap();
}
snapshot!(ctx, @r#"
- name: http.client.open_connections
description: Number of open connections in the HTTP client pool
unit: "{connection}"
data:
type: Sum
data_points:
- attributes:
http.connection.state: active
network.protocol.version: "2"
server.address: 127.0.0.1
server.port: "<port>"
value: 0
- attributes:
http.connection.state: idle
network.protocol.version: "2"
server.address: 127.0.0.1
server.port: "<port>"
value: 1
is_monotonic: false
temporality: Cumulative
"#);
drop(client);
snapshot!(ctx, @r#"
- name: http.client.open_connections
description: Number of open connections in the HTTP client pool
unit: "{connection}"
data:
type: Sum
data_points:
- attributes:
http.connection.state: active
network.protocol.version: "2"
server.address: 127.0.0.1
server.port: "<port>"
value: 0
- attributes:
http.connection.state: idle
network.protocol.version: "2"
server.address: 127.0.0.1
server.port: "<port>"
value: 0
is_monotonic: false
temporality: Cumulative
"#);
}
#[tokio::test]
async fn h2_connection_duration_is_recorded() {
let ctx = TelemetryContext::new();
let addr = spawn_h2c_server(Router::new().route("/", get(|| async { "ok" }))).await;
let req = http::Request::builder()
.method(http::Method::GET)
.uri(format!("http://{addr}/"))
.body(empty_body())
.unwrap();
new_client(&h2_config())
.oneshot(req)
.await
.expect("request ok");
assert_metric!(
ctx,
"http.client.connection.duration",
"network.protocol.version" = "2"
);
}
#[tokio::test]
async fn h2_connections_freed_immediately_on_client_drop() {
let ctx = TelemetryContext::new();
let addr = spawn_h2c_server(Router::new().route("/", get(|| async { "ok" }))).await;
let client = new_client(&h2_config());
let req = http::Request::builder()
.method(http::Method::GET)
.uri(format!("http://{addr}/"))
.body(empty_body())
.unwrap();
let resp = client.clone().oneshot(req).await.expect("request ok");
resp.into_body().collect().await.unwrap();
drop(client);
assert_metric!(
ctx,
"http.client.connection.duration",
"network.protocol.version" = "2"
);
}
#[tokio::test]
async fn h2_does_not_derive_host_header() {
let _ctx = TelemetryContext::new();
let router = Router::new().route(
"/",
get(async move |req: axum::http::Request<axum::body::Body>| {
assert_eq!(
req.headers().get("host"),
None,
"should not have host header in http/2"
);
"hello world"
}),
);
let addr = spawn_h2c_server(router).await;
let client = new_client(&h2_config());
let req = http::Request::builder()
.method(http::Method::GET)
.uri(format!("http://{addr}/"))
.body(empty_body())
.unwrap();
let resp = client.oneshot(req).await.expect("request ok");
let body = resp.into_body().collect().await.expect("should read body");
let body = String::from_utf8(body.to_bytes().to_vec()).expect("body should be utf8");
assert_eq!(body, "hello world", "received the expected response");
}