Task 2: Implement the /metrics
and /metrics/{kind}
endpoints
For checking the system metrics, we'll need to a little design before. So, we want to get all the metrics at once, and we also want to get specific metrics. For this, we'll have two endpoints:
GET /metrics
: Retrieve a comprehensive summary of system metrics.GET /metrics/{kind}
: Fetch specific metrics (e.g., system, process, memory, cpu, or disk) to drill down into performance data.
To register the new endpoints, follow the steps below:
- create a new module
src/routes/metrics.rs
and add the following code:
use axum::{Router, extract::Path, http::StatusCode, response::IntoResponse, routing::get};
pub fn register() -> Router {
Router::new()
.route("/", get(get_metrics))
.route("/{kind}", get(get_metric))
}
async fn get_metrics() -> impl IntoResponse {
todo!("Implement the get_metrics endpoint")
}
async fn get_metric(kind: /* TODO */) -> impl IntoResponse {
todo!("Implement the get_metric endpoint")
}
- Update
src/routes/mod.rs
to include the new module:
use axum::Router;
mod healthcheck;
mod metrics;
pub fn app() -> Router {
Router::new()
.nest("/healthcheck", healthcheck::register())
.nest("/metrics", metrics::register())
}
Also, we should restrict the values that kind
can take. To do so, we will represent them within the type system as an enum, like such:
- Create a new module
src/metrics.rs
. - Add the following code to the new file:
pub enum Kind {
System,
Process,
Memory,
Cpu,
Disk,
}
- Add the following line to
main.rs
:
mod metrics;
- Finally, we'll update the
get_metric
handler to take aKind
parameter:
// ...
use crate::metrics;
async fn get_metric(Path(kind): Path<metrics::Kind>) -> impl IntoResponse {
todo!("Implement the get_metric endpoint")
}
Path is a type provided by Axum that allows you to extract a part of the request path. In this case, we're using it to extract the
kind
parameter from the request path.
Ok so we designed the request format and the endpoints, now, let's implement the logic to get the metrics.
I've created some helper functions in src/metrics.rs
to get the system metrics. You can use them to implement the /metrics
and /metrics/{kind}
endpoints.
use sysinfo;
pub async fn init() -> sysinfo::System {
let mut sys = sysinfo::System::new_all();
sys.refresh_all();
tokio::time::sleep(sysinfo::MINIMUM_CPU_UPDATE_INTERVAL).await;
sys.refresh_cpu_all();
sys
}
pub enum Kind {
System,
Process,
Memory,
Cpu,
Disk,
}
pub struct System {
name: String,
kernel_version: String,
os_version: String,
host_name: String,
uptime: u64,
}
impl System {
pub fn generate() -> Self {
todo!("Implement the System::generate method")
}
}
pub struct Process {
pid: u32,
name: String,
memory: u64,
cpu_usage: f32,
run_time: u64,
}
impl Process {
pub fn generate(sys: &mut sysinfo::System) -> Vec<Self> {
todo!("Implement the Process::generate method")
}
}
#[derive(serde::Serialize, serde::Deserialize)]
pub struct Memory {
used: u64,
total: u64,
}
impl Memory {
pub fn generate(sys: &mut sysinfo::System) -> Self {
todo!("Implement the Memory::generate method")
}
}
#[derive(serde::Serialize, serde::Deserialize)]
pub struct CoreMetrics {
name: String,
brand: String,
usage: f32,
frequency: u64,
}
#[derive(serde::Serialize, serde::Deserialize)]
pub struct Cpu {
cpu_usage: f32,
cores: Vec<CoreMetrics>,
}
impl Cpu {
pub fn generate(sys: &mut sysinfo::System) -> Self {
todo!("Implement the Cpu::generate method")
}
}
#[derive(serde::Serialize, serde::Deserialize)]
pub struct Disk {
name: String,
available_space: u64,
total_space: u64,
is_removable: bool,
}
impl Disk {
pub fn generate() -> Vec<Self> {
todo!("Implement the Disk::generate method")
}
}
#[derive(serde::Serialize, serde::Deserialize)]
pub struct Summary {
system: System,
process: Vec<Process>,
memory: Memory,
cpu: Cpu,
disk: Vec<Disk>,
}
impl Summary {
pub fn generate(sys: &mut sysinfo::System) -> Self {
todo!("Implement the Summary::generate method")
}
}
To get the system metrics, we're using the
sysinfo
crate. It provides a simple interface to get system information like CPU usage, memory usage, disk space, etc. You can find more information about the crate here. Install the crate similar to how you installed Axum and Tokio.
What is serde?​
serde
is a popular Rust library for serializing and deserializing data. It provides a simple way to convert Rust data structures into JSON, XML, or other formats. In this project, we're using serde
to serialize our metrics into JSON format.
The name
serde
comes from "serialization" and "deserialization."
Because our HTTP server uses JSON as the default format for responses, we need to implement the serde::Serialize
trait for our metric structs. This trait allows us to convert our structs into JSON objects that can be sent over the network.
To add serde to our project, run the following commands in a terminal:
cargo add serde --features derive
cargo add serde_json
To convert a struct into JSON, you can use the serde_json::to_string
function. For example:
serde_json::to_string(&my_struct).unwrap()
Your task​
As task, implement the get_metrics
and get_metric
functions to return a 200 OK
status code with the system metrics. The get_metrics
function should return a summary of all the metrics, while the get_metric
function should return the specific metric based on the kind
parameter.
Also, implement the generate
methods for the System
, Process
, Memory
, Cpu
, Disk
, and Summary
structs to generate the metrics.
Conclusion​
In this task, we've designed the /metrics
and /metrics/{kind}
endpoints and implemented the logic to get the system metrics. By following the steps above, you've learned how to structure your project, define routes, and handle requests in Axum. Next, we'll add the /realtime
endpoint to stream live metric updates using Server-Sent Events (SSE). Let's continue building our system monitor server! 🚀