use bytes::Bytes;
use dragonfly_client_util::shutdown;
use http_body_util::Full;
use hyper::server::conn::http1::Builder as ServerBuilder;
use hyper::service::service_fn;
use hyper::{Method, Request, Response, StatusCode};
use hyper_util::rt::TokioIo;
use pprof::protos::Message;
use pprof::ProfilerGuard;
use std::convert::Infallible;
use std::net::SocketAddr;
use std::time::Duration;
use tokio::net::TcpListener;
use tokio::sync::mpsc;
use tracing::{error, info, instrument};
const DEFAULT_PROFILER_SECONDS: u64 = 10;
const DEFAULT_PROFILER_FREQUENCY: i32 = 1000;
pub struct PProfProfileQueryParams {
pub seconds: u64,
pub frequency: i32,
}
impl Default for PProfProfileQueryParams {
fn default() -> Self {
Self {
seconds: DEFAULT_PROFILER_SECONDS,
frequency: DEFAULT_PROFILER_FREQUENCY,
}
}
}
impl PProfProfileQueryParams {
fn from_query(query: &str) -> Self {
let mut params = Self::default();
for (key, value) in query.split('&').filter_map(|pair| pair.split_once('=')) {
match key {
"seconds" => params.seconds = value.parse().unwrap_or(params.seconds),
"frequency" => params.frequency = value.parse().unwrap_or(params.frequency),
_ => {}
}
}
params
}
}
#[derive(Debug)]
pub struct Stats {
addr: SocketAddr,
shutdown: shutdown::Shutdown,
_shutdown_complete: mpsc::UnboundedSender<()>,
}
impl Stats {
pub fn new(
addr: SocketAddr,
shutdown: shutdown::Shutdown,
shutdown_complete_tx: mpsc::UnboundedSender<()>,
) -> Self {
Self {
addr,
shutdown,
_shutdown_complete: shutdown_complete_tx,
}
}
pub async fn run(&self) {
let mut shutdown = self.shutdown.clone();
info!("stats server listening on {}", self.addr);
let listener = TcpListener::bind(self.addr).await.unwrap();
loop {
tokio::select! {
tcp_accepted = listener.accept() => {
let (tcp, remote_address) = match tcp_accepted {
Ok(tcp_accepted) => tcp_accepted,
Err(err) => {
error!("failed to accept connection: {}", err);
continue;
}
};
let io = TokioIo::new(tcp);
tokio::spawn(async move {
if let Err(err) = ServerBuilder::new()
.serve_connection(io, service_fn(Self::handler))
.await
{
error!("failed to serve connection from {}: {}", remote_address, err);
}
});
}
_ = shutdown.recv() => {
info!("stats server shutting down");
return;
}
}
}
}
#[instrument(skip_all)]
async fn handler<T>(request: Request<T>) -> Result<Response<Full<Bytes>>, Infallible> {
match (request.method(), request.uri().path()) {
(&Method::GET, "/debug/pprof/profile") => {
let query_params = request
.uri()
.query()
.map(PProfProfileQueryParams::from_query)
.unwrap_or_default();
match Self::pprof_profile_handler(query_params).await {
Ok(body) => Ok(Response::new(Full::new(Bytes::from(body)))),
Err(()) => Ok(Response::builder()
.status(StatusCode::INTERNAL_SERVER_ERROR)
.body(Full::default())
.unwrap()),
}
}
(&Method::GET, "/debug/pprof/heap") => match Self::pprof_heap_handler().await {
Ok(body) => Ok(Response::new(Full::new(Bytes::from(body)))),
Err(()) => Ok(Response::builder()
.status(StatusCode::INTERNAL_SERVER_ERROR)
.body(Full::default())
.unwrap()),
},
_ => Ok(Response::builder()
.status(StatusCode::NOT_FOUND)
.body(Full::default())
.unwrap()),
}
}
#[instrument(skip_all)]
async fn pprof_profile_handler(query_params: PProfProfileQueryParams) -> Result<Vec<u8>, ()> {
info!(
"start profiling for {} seconds with {} frequency",
query_params.seconds, query_params.frequency
);
let guard = ProfilerGuard::new(query_params.frequency).map_err(|err| {
error!("failed to create profiler guard: {}", err);
})?;
tokio::time::sleep(Duration::from_secs(query_params.seconds)).await;
let report = guard.report().build().map_err(|err| {
error!("failed to build profiler report: {}", err);
})?;
let profile = report.pprof().map_err(|err| {
error!("failed to get pprof profile: {}", err);
})?;
let mut body: Vec<u8> = Vec::new();
profile.write_to_vec(&mut body).map_err(|err| {
error!("failed to write pprof profile: {}", err);
})?;
Ok(body)
}
#[instrument(skip_all)]
async fn pprof_heap_handler() -> Result<Vec<u8>, ()> {
info!("start heap profiling");
#[cfg(target_os = "linux")]
{
let Some(prof_ctl) = jemalloc_pprof::PROF_CTL.as_ref() else {
return Err(());
};
let mut prof_ctl = prof_ctl.lock().await;
if !prof_ctl.activated() {
return Err(());
}
let pprof = prof_ctl.dump_pprof().map_err(|err| {
error!("failed to dump pprof: {}", err);
})?;
Ok(pprof)
}
#[cfg(not(target_os = "linux"))]
Err(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::net::{IpAddr, Ipv4Addr};
#[test]
fn test_pprof_profile_query_params_default() {
let params = PProfProfileQueryParams::default();
assert_eq!(params.seconds, DEFAULT_PROFILER_SECONDS);
assert_eq!(params.frequency, DEFAULT_PROFILER_FREQUENCY);
}
#[test]
fn test_pprof_profile_query_params_from_query() {
let params = PProfProfileQueryParams::from_query("seconds=25&frequency=1500");
assert_eq!(params.seconds, 25);
assert_eq!(params.frequency, 1500);
let params = PProfProfileQueryParams::from_query("seconds=25");
assert_eq!(params.seconds, 25);
assert_eq!(params.frequency, DEFAULT_PROFILER_FREQUENCY);
let params = PProfProfileQueryParams::from_query("seconds=invalid&unknown=1");
assert_eq!(params.seconds, DEFAULT_PROFILER_SECONDS);
assert_eq!(params.frequency, DEFAULT_PROFILER_FREQUENCY);
}
#[test]
fn test_stats_new() {
let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 8080);
let shutdown = shutdown::Shutdown::new();
let (shutdown_complete_tx, _shutdown_complete_rx) = mpsc::unbounded_channel();
let stats = Stats::new(addr, shutdown, shutdown_complete_tx);
assert_eq!(stats.addr, addr);
}
#[tokio::test]
async fn test_pprof_profile_handler_with_default_params() {
let params = PProfProfileQueryParams {
seconds: 0,
frequency: 1000,
};
let result = Stats::pprof_profile_handler(params).await;
let _ = result;
}
#[tokio::test]
async fn test_pprof_profile_handler_with_custom_frequency() {
let params = PProfProfileQueryParams {
seconds: 0,
frequency: 500,
};
let result = Stats::pprof_profile_handler(params).await;
let _ = result;
}
#[cfg(not(target_os = "linux"))]
#[tokio::test]
async fn test_pprof_heap_handler_non_linux() {
let result = Stats::pprof_heap_handler().await;
assert!(result.is_err());
}
}