Skip to main content

krabka_telemetry/profiling/
server.rs

1//! Binding and supervising the admin HTTP server that serves the pprof routes.
2//!
3//! The module holds the `serve_*` and `spawn_*` entry points, the shared bind
4//! step behind them, and the `KRABKA_ADMIN_LISTEN_ADDR` lookup that the
5//! environment-address variants use. It is separate from the routes so that
6//! the listener lifecycle stays in one place.
7
8use {
9    super::{
10        config::{ProfilingConfig, ProfilingError},
11        routes::{pprof_router, pprof_router_with_config},
12    },
13    axum::Router,
14    std::{
15        env,
16        io::{self, Error},
17        net::SocketAddr,
18    },
19    tokio::{net::TcpListener, task::JoinHandle},
20};
21
22/// Bind an admin HTTP server on `addr`.
23///
24/// The server serves `pprof_router()` merged with `extra`, for example a
25/// `/metrics` route. This function spawns the server and returns after the
26/// bind.
27/// # Errors
28/// Returns an error if the listener cannot bind or the HTTP server fails.
29pub async fn serve_admin(addr: SocketAddr, extra: Router) -> io::Result<()> {
30    serve_router(addr, pprof_router().merge(extra)).await
31}
32
33async fn serve_router(addr: SocketAddr, app: Router) -> io::Result<()> {
34    let task = spawn_router(addr, app).await?;
35    tokio::spawn(async move {
36        match task.await {
37            Ok(Ok(())) => tracing::warn!("admin server stopped unexpectedly"),
38            Ok(Err(error)) => tracing::warn!(%error, "admin server error"),
39            Err(error) => tracing::warn!(%error, "admin server task failed"),
40        }
41    });
42    Ok(())
43}
44
45async fn spawn_router(addr: SocketAddr, app: Router) -> io::Result<JoinHandle<io::Result<()>>> {
46    let listener = TcpListener::bind(addr).await?;
47    let bound = listener.local_addr()?;
48    tracing::info!(%bound, "profiling admin server listening");
49    Ok(tokio::spawn(
50        async move { axum::serve(listener, app).await },
51    ))
52}
53
54/// Bind a profiling admin server with explicit policy.
55///
56/// # Errors
57/// Returns an error for invalid profiling configuration or listener failure.
58pub async fn serve_admin_with_config(
59    addr: SocketAddr,
60    extra: Router,
61    config: ProfilingConfig,
62) -> Result<(), ProfilingError> {
63    let app = pprof_router_with_config(config)?.merge(extra);
64    serve_router(addr, app).await?;
65    Ok(())
66}
67
68/// Bind a profiling admin server and return its task for lifecycle supervision.
69///
70/// # Errors
71/// Returns an error for invalid profiling configuration or listener failure.
72pub async fn spawn_admin_with_config(
73    addr: SocketAddr,
74    extra: Router,
75    config: ProfilingConfig,
76) -> Result<JoinHandle<io::Result<()>>, ProfilingError> {
77    let app = pprof_router_with_config(config)?.merge(extra);
78    Ok(spawn_router(addr, app).await?)
79}
80
81/// Wait for a supervised admin task, treating every terminal outcome as an error.
82///
83/// # Errors
84/// Returns the server error, join error, or an unexpected clean-exit error.
85pub async fn await_admin_exit(task: JoinHandle<io::Result<()>>) -> io::Result<()> {
86    match task.await {
87        Ok(Ok(())) => Err(Error::other("admin server stopped unexpectedly")),
88        Ok(Err(error)) => Err(error),
89        Err(error) => Err(Error::other(format!("admin server task failed: {error}"))),
90    }
91}
92
93fn admin_address(default_addr: &str) -> SocketAddr {
94    let raw = env::var("KRABKA_ADMIN_LISTEN_ADDR").unwrap_or_else(|_| default_addr.to_owned());
95    raw.parse()
96        .unwrap_or_else(|e| panic!("invalid KRABKA_ADMIN_LISTEN_ADDR `{raw}`: {e}"))
97}
98
99/// Like [`serve_admin`], but with the bind address from the environment.
100///
101/// This function reads `KRABKA_ADMIN_LISTEN_ADDR` and falls back to
102/// `default_addr`.
103/// # Errors
104/// Returns an error if the listener cannot bind or the HTTP server fails.
105///
106/// # Panics
107///
108/// Panics if the environment variable, or `default_addr` when it is absent,
109/// is not a socket address.
110pub async fn serve_admin_from_env(default_addr: &str) -> io::Result<()> {
111    serve_admin_from_env_with(default_addr, Router::new()).await
112}
113
114/// Like [`serve_admin_from_env`], but it also merges `extra` with the pprof routes.
115///
116/// `extra` is, for example, a `GET /metrics` route. Services that expose
117/// Prometheus metrics call this function with their `/metrics` router, and the
118/// exporter thus shares the admin port.
119/// # Errors
120/// Returns an error if the listener cannot bind or the HTTP server fails.
121/// # Panics
122/// Panics if `KRABKA_ADMIN_LISTEN_ADDR`, or `default_addr` when it is absent,
123/// is not a socket address. Router merging also panics if `extra` conflicts
124/// with an existing profiling route.
125pub async fn serve_admin_from_env_with(default_addr: &str, extra: Router) -> io::Result<()> {
126    let addr = admin_address(default_addr);
127    serve_admin(addr, extra).await
128}
129
130/// Like [`serve_admin_from_env_with`] with explicit profiling policy.
131///
132/// # Errors
133/// Returns an error for invalid profiling configuration or listener failure.
134///
135/// # Panics
136/// Panics when `KRABKA_ADMIN_LISTEN_ADDR`, or the fallback `default_addr`, is not a socket address. This
137/// behavior is the same as the default-compatible wrapper's behavior.
138pub async fn serve_admin_from_env_with_config(
139    default_addr: &str,
140    extra: Router,
141    config: ProfilingConfig,
142) -> Result<(), ProfilingError> {
143    let addr = admin_address(default_addr);
144    serve_admin_with_config(addr, extra, config).await
145}
146
147/// Environment-address variant of [`spawn_admin_with_config`].
148///
149/// # Errors
150/// Returns an error for invalid profiling configuration or listener failure.
151///
152/// # Panics
153/// Panics when `KRABKA_ADMIN_LISTEN_ADDR`, or the fallback `default_addr`, is not a socket address.
154pub async fn spawn_admin_from_env_with_config(
155    default_addr: &str,
156    extra: Router,
157    config: ProfilingConfig,
158) -> Result<JoinHandle<io::Result<()>>, ProfilingError> {
159    let addr = admin_address(default_addr);
160    spawn_admin_with_config(addr, extra, config).await
161}
162
163#[cfg(test)]
164mod tests {
165    use {super::*, assert2::assert, std::io::Error};
166
167    #[tokio::test]
168    async fn supervised_admin_exit_classifies_clean_error_and_join_outcomes() {
169        let clean = tokio::spawn(async { Ok(()) });
170        assert!(
171            await_admin_exit(clean).await.unwrap_err().to_string()
172                == "admin server stopped unexpectedly"
173        );
174
175        let io_error = tokio::spawn(async { Err(Error::other("socket failed")) });
176        assert!(await_admin_exit(io_error).await.unwrap_err().to_string() == "socket failed");
177
178        let panic = tokio::spawn(async {
179            panic!("admin panic");
180            #[allow(unreachable_code)]
181            Ok(())
182        });
183        assert!(
184            await_admin_exit(panic)
185                .await
186                .unwrap_err()
187                .to_string()
188                .starts_with("admin server task failed:")
189        );
190    }
191}