Skip to main content

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:

  1. GET /metrics: Retrieve a comprehensive summary of system metrics.
  2. 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:

  1. create a new module src/routes/metrics.rs and add the following code:
src/routes/metrics.rs

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")
}
  1. Update src/routes/mod.rs to include the new module:
src/routes/mod.rs
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:

  1. Create a new module src/metrics.rs.
  2. Add the following code to the new file:
src/metrics.rs
pub enum Kind {
System,
Process,
Memory,
Cpu,
Disk,
}
  1. Add the following line to main.rs:
src/main.rs
mod metrics;
  1. Finally, we'll update the get_metric handler to take a Kind parameter:
src/routes/metrics.rs
// ...

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.

src/metrics.rs
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! 🚀