#![cfg_attr(
not(any(feature = "reqwest-0-12", feature = "reqwest-0-13")),
allow(dead_code)
)]
use crossbeam_channel::{bounded, Receiver as CbReceiver, RecvTimeoutError, Sender as CbSender};
use hdrhistogram::Histogram;
use std::collections::{HashMap, VecDeque};
use std::sync::atomic::{AtomicU32, Ordering};
use std::sync::{Arc, Mutex as StdMutex, OnceLock};
use crate::lib_on::{meta_rw_lock, MetaRwLock};
use crate::batch::{EventProducer, EventQueueRegistry};
use crate::instant::Instant;
use crate::json::{HttpLogEntry, HttpLogs};
use crate::lib_on::hotpath_guard::{DRAIN_INTERVAL_MS, LOGS_LIMIT};
use crate::lib_on::START_TIME;
use crate::metrics_server::METRICS_SERVER_PORT;
pub(crate) mod normalize;
#[cfg(feature = "reqwest-0-12")]
mod reqwest_012;
#[cfg(feature = "reqwest-0-13")]
mod reqwest_013;
static HTTP_ID_COUNTER: AtomicU32 = AtomicU32::new(1);
fn next_http_id() -> u32 {
HTTP_ID_COUNTER.fetch_add(1, Ordering::Relaxed)
}
#[derive(Default, Clone)]
pub struct ReqwestHttpMiddleware {
pub(crate) label: Option<String>,
}
impl ReqwestHttpMiddleware {
pub fn new() -> Self {
init_http_state();
Self::default()
}
pub fn with_label(label: impl Into<String>) -> Self {
init_http_state();
Self {
label: Some(label.into()),
}
}
}
pub trait InstrumentHttpClient {
type Output;
fn instrument_http(self, label: Option<String>) -> Self::Output;
}
#[derive(Debug)]
pub(crate) enum HttpEvent {
Executed {
endpoint: Arc<str>,
label: Option<Arc<str>>,
duration_nanos: u64,
status: Option<u16>,
timestamp_ns: u64,
source: Option<&'static str>,
},
}
#[derive(Debug, Clone)]
pub(crate) struct HttpEntry {
pub(crate) id: u32,
pub(crate) endpoint: String,
pub(crate) source: Option<&'static str>,
pub(crate) count: u64,
pub(crate) error_count: u64,
pub(crate) total_nanos: u64,
hist: Option<Histogram<u64>>,
}
impl HttpEntry {
const LOW_NS: u64 = 1;
const HIGH_NS: u64 = 1_000_000_000_000; const SIGFIGS: u8 = 3;
fn new(id: u32, endpoint: String, source: Option<&'static str>) -> Self {
Self {
id,
endpoint,
source,
count: 0,
error_count: 0,
total_nanos: 0,
hist: Histogram::<u64>::new_with_bounds(Self::LOW_NS, Self::HIGH_NS, Self::SIGFIGS)
.ok(),
}
}
#[inline]
fn record(&mut self, nanos: u64) {
if let Some(ref mut hist) = self.hist {
hist.record(nanos.clamp(Self::LOW_NS, Self::HIGH_NS))
.unwrap();
}
}
pub(crate) fn avg_nanos(&self) -> u64 {
self.total_nanos.checked_div(self.count).unwrap_or(0)
}
pub(crate) fn percentile_nanos(&self, p: f64) -> u64 {
match self.hist {
Some(ref hist) if self.count > 0 => hist.value_at_percentile(p.clamp(0.0, 100.0)),
_ => 0,
}
}
}
pub(crate) struct HttpInternalState {
pub(crate) stats: HashMap<(Option<&'static str>, String), HttpEntry>,
pub(crate) logs: HashMap<u32, VecDeque<HttpLogEntry>>,
}
pub(crate) struct HttpState {
pub(crate) inner: Arc<MetaRwLock<HttpInternalState>>,
pub(crate) shutdown_tx: StdMutex<Option<CbSender<()>>>,
pub(crate) completion_rx: StdMutex<Option<CbReceiver<()>>>,
}
pub(crate) static HTTP_STATE: OnceLock<HttpState> = OnceLock::new();
pub(crate) fn get_sorted_http_entries() -> Vec<HttpEntry> {
let Some(state) = HTTP_STATE.get() else {
return Vec::new();
};
let guard = state.inner.read().unwrap();
let mut stats: Vec<HttpEntry> = guard.stats.values().cloned().collect();
stats.sort_by(compare_http_entries);
stats
}
pub(crate) fn get_http_logs(id: u32) -> Option<HttpLogs> {
let state = HTTP_STATE.get()?;
let guard = state.inner.read().unwrap();
let logs = guard.logs.get(&id)?;
Some(HttpLogs {
id,
logs: logs.iter().rev().cloned().collect(),
})
}
pub(crate) fn get_http_json() -> crate::json::JsonHttpList {
let entries = get_sorted_http_entries();
let elapsed = std::time::Duration::from_nanos(crate::lib_on::current_elapsed_ns());
let reference_total: u64 = entries.iter().map(|e| e.total_nanos).sum();
crate::lib_on::report::collect_http_json(
&entries,
elapsed,
reference_total,
&crate::lib_on::hotpath_guard::configured_percentiles(),
)
}
static EVENT_QUEUES: EventQueueRegistry<HttpEvent> = EventQueueRegistry::new();
thread_local! {
static EVENT_PRODUCER: EventProducer<HttpEvent> = EVENT_QUEUES.register();
}
#[inline]
pub(crate) fn send_http_event(event: HttpEvent) {
if !EVENT_QUEUES.is_active() {
return;
}
let _suspend = crate::lib_on::SuspendAllocTracking::new();
let _ = EVENT_PRODUCER.try_with(|producer| producer.push(event));
}
pub(crate) fn stop_http_events() {
EVENT_QUEUES.set_active(false);
}
fn process_http_event(state: &mut HttpInternalState, event: HttpEvent) {
let HttpEvent::Executed {
endpoint,
label,
duration_nanos,
status,
timestamp_ns,
source,
} = event;
let mut key = normalize::normalize_endpoint(&endpoint);
if let Some(label) = label {
key = format!("{label}: {key}");
}
let entry = state
.stats
.entry((source, key))
.or_insert_with_key(|(source, endpoint)| {
HttpEntry::new(next_http_id(), endpoint.clone(), *source)
});
entry.count += 1;
entry.total_nanos += duration_nanos;
if status.is_none() || status.is_some_and(|s| s >= 400) {
entry.error_count += 1;
}
entry.record(duration_nanos);
let logs = state.logs.entry(entry.id).or_default();
if logs.len() >= *LOGS_LIMIT {
logs.pop_front();
}
logs.push_back(HttpLogEntry {
index: entry.count,
timestamp: timestamp_ns,
duration_nanos,
status,
});
}
fn flush_http_buffer(buffer: &mut Vec<HttpEvent>, inner: &Arc<MetaRwLock<HttpInternalState>>) {
if buffer.is_empty() {
return;
}
if let Ok(mut shared) = inner.write() {
for e in buffer.drain(..) {
process_http_event(&mut shared, e);
}
}
}
pub fn init_http_state() {
HTTP_STATE.get_or_init(|| {
START_TIME.get_or_init(Instant::now);
let (shutdown_tx, shutdown_rx) = bounded::<()>(1);
let (completion_tx, completion_rx) = bounded::<()>(1);
let inner = Arc::new(meta_rw_lock!(
"http_state",
HttpInternalState {
stats: HashMap::new(),
logs: HashMap::new(),
},
));
let inner_clone = Arc::clone(&inner);
EVENT_QUEUES.set_active(true);
std::thread::Builder::new()
.name("hp-http".into())
.spawn(move || {
let flush_interval = std::time::Duration::from_millis(*DRAIN_INTERVAL_MS);
let mut swept: Vec<HttpEvent> = Vec::new();
loop {
let shutdown = !matches!(
shutdown_rx.recv_timeout(flush_interval),
Err(RecvTimeoutError::Timeout)
);
if shutdown {
EVENT_QUEUES.drain_all(&mut swept);
flush_http_buffer(&mut swept, &inner_clone);
break;
}
EVENT_QUEUES.sweep(&mut swept);
flush_http_buffer(&mut swept, &inner_clone);
}
let _ = completion_tx.send(());
})
.expect("Failed to spawn http-stats-collector thread");
crate::metrics_server::start_metrics_server_once(*METRICS_SERVER_PORT);
HttpState {
inner,
shutdown_tx: StdMutex::new(Some(shutdown_tx)),
completion_rx: StdMutex::new(Some(completion_rx)),
}
});
}
pub(crate) fn compare_http_entries(a: &HttpEntry, b: &HttpEntry) -> std::cmp::Ordering {
b.total_nanos
.cmp(&a.total_nanos)
.then_with(|| b.count.cmp(&a.count))
.then_with(|| a.id.cmp(&b.id))
}
pub(crate) fn endpoint_pre_key(
method: &str,
host: Option<&str>,
port: Option<u16>,
path: &str,
) -> String {
let host = host.unwrap_or("");
match port {
Some(port) => format!("{method} {host}:{port}{path}"),
None => format!("{method} {host}{path}"),
}
}
#[macro_export]
macro_rules! http {
($client:expr) => {{
$crate::http::init_http_state();
$crate::InstrumentHttpClient::instrument_http($client, None)
}};
($client:expr, label = $label:expr) => {{
$crate::http::init_http_state();
$crate::InstrumentHttpClient::instrument_http($client, Some($label.to_string()))
}};
}