About this Rust Practical Tutorial Series:
- Positioning: A record of exercises transitioning from Rust basics to practical projects.
- Content: Focused on specific problems encountered in practice, solutions, and insights.
- Readers: Suitable for developers with a basic understanding of Rust who urgently need project experience to consolidate their knowledge. Experienced players may skip this.
- Plan: This issue is the second part of the series, which will be continuously updated based on my learning progress.
- Navigation: Rust Practical (Part 1): Development and Architecture Analysis of Command Line File Manager, Rust Practical (Part 2): Network Resource Monitor (Initial Version) – Building the Monitoring Core from Scratch
- Foreword: Apologies for my ignorance; I previously claimed to build an enterprise-level monitoring system, but my naivety limited my ideas. I didn’t expect that the simple HTTP monitoring I had implemented before would start to hit a wall. I didn’t realize that a single HTTP monitor could monitor so much content. The following is a relatively simple and incomplete monitoring system. Currently, I have completed the HTTP monitoring part and the overall framework content. My skills are limited, and I hope the experts will forgive me. 😞😞😞
Brief Description
This article is a continuation of the previous network resource monitor (initial version). In this article, I will continue to enrich the functionality of the network resource monitor. Of course, this article is not the end; it is just the second version because the complete functionality is indeed too much. This series is also a record of my learning. If completed all at once, it would be too complex and not very friendly for a beginner like me. Therefore, I will complete each module’s content bit by bit, striving to create a usable enterprise-level network resource monitor (Let’s hype it up~);
Requirements
1. Build the framework of the monitoring engine
- Support for multiple monitoring types (HTTP/HTTPS, Memory, TCP, PING, etc.)
- Configurable check frequency
- Concurrent execution of monitoring checks
- Retry mechanism
- Timeout control
- Result recording (including response time, status, error information, etc.)
2. Implement the HTTP/HTTPS monitoring engine
- Basic availability monitoring (BasicAvailability)
- Performance monitoring (PerformanceTimings)
- Security monitoring (SslSecurity, SecurityHeaders)
- Content verification monitoring (ContentVerification)
Feature Point Details
First, we will build a framework that supports multiple monitoring types based on the monitoring configuration list from the previous content. In this course, we will gradually complete the basic structure of the overall framework and focus on implementing the monitoring logic for HTTP/HTTPS types. Other monitoring types (such as ICMP, TCP, DNS, etc.) are temporarily not included in this implementation and will be left as after-class extension exercises for everyone to complete and improve themselves (Actually, it’s mainly because there are too many monitoring types, and I can’t write them all~).
Alright, let’s clarify the core tasks. In simple terms, this framework needs to support multiple monitoring types (such as HTTP, HTTPS, DNS, TCP, etc.) and use a monitoring engine to uniformly schedule these check tasks, ultimately summarizing and outputting the monitoring results of all types.
To achieve this goal, we can divide the overall logic into the following two core parts:
- Monitoring Engine: This is the brain of the framework, responsible for scheduling and executing tasks. Its main job is to read the monitoring list, sequentially call the check logic of different monitoring types, and collect results.
- Monitoring Type Implementation: This is the muscle of the framework, containing the specific check logic for various monitoring types (such as HTTP/HTTPS). Each type is an independent module responsible for executing specialized checks and returning standardized results.
Next, we will implement these two modules specifically:
Multi-Type Monitoring Engine Implementation
In this framework design, we primarily adopt a combination of strategy pattern and factory pattern architecture scheme:
- Strategy Pattern: We define each monitoring type (such as HTTP, HTTPS) as an independent strategy that implements a unified
<span>Monitor</span>Trait. This ensures that each monitoring check logic is highly cohesive and can be developed and evolved independently. - Factory Pattern: We use a unified factory to batch create corresponding monitor instances based on configuration. This eliminates complex object creation logic in the code, achieving decoupling between the client and the specific implementation.
Thus, expanding new monitoring types (such as DNS, TCP) will become exceptionally simple:Just define a new strategy and implement the <span>Monitor</span> Trait, without modifying the core scheduling logic of the engine. High cohesion, low coupling, perfect~~
1. Implementation of the Public Monitor Trait
First, let’s look at the definition of our unified Trait. This trait object is quite simple; the core only needs to implement a check function.
// src/monitor/monitor_trait.rs
use super::types::{MonitorConfig, CheckResult};
use crate::tools_types::MonitorType;
// Define the trait for monitoring types, and later different monitoring types will implement this trait
// Add Send and Sync as parent traits here
// Send: allows moving between threads
// Sync: allows sharing references between threads (&T)
// For tokio::spawn and multi-threaded async, these two are usually required.
#[async_trait::async_trait]
pub trait Monitor: Send + Sync {
async fn check(&self, config: &MonitorConfig) -> CheckResult;
fn get_type(&self) -> MonitorType;
}
The corresponding <span>check</span> function only needs to pass in a monitoring configuration parameter and return a check result. We also defined a <span>get_type</span> method: to get the current detection type. This way, we have already completed the task of defining our unified trait object. Next, let’s look at the definition logic of the two objects we abstracted out: <span>MonitorConfig</span> and <span>CheckResult</span>: Here, I declared a <span>types.rs</span> module specifically to declare the corresponding struct objects, enum values, and other basic type definitions used in the process.
1.1 Monitor Configuration Parameter MonitorConfig
// src/monitor/types.rs Type declaration module
// Define the core monitoring configuration parameter struct
#[derive(Debug, Clone)]
pub struct MonitorConfig {
pub target: Option<String>, // The target URL or IP address to monitor
pub interval: Option<u64>, // Monitoring interval, in seconds
pub monitor_type: MonitorType,// Monitoring type HTTP TCP FTP, etc.
pub details: MonitorConfigDetail, // Here are the specific parameters for different monitoring types, distinguished from public parameters, see line 14 for declaration
}
// Define a struct for monitoring data detail parameters, used for different parameter types in each monitoring type
#[derive(Debug, Clone)]
pub enum MonitorConfigDetail {
Http(HttpMonitorConfig),
Https(HttpsMonitorConfig),
Ftp(FtpMonitorConfig),
Traceroute(TracerouteMonitorConfig),
Cpu(CpuMonitorConfig),
Memory(MemoryMonitorConfig),
Disk(DiskMonitorConfig),
Process(ProcessMonitorConfig),
Unknown(UnknownQueryConfig),
}
// Among these different monitoring type structs, we will not declare them one by one; they are all similar,
// Exception monitoring fallback type
#[derive(Debug, Clone)]
pub struct UnknownQueryConfig{
pub description: String, // Exception description
}
// HTTP monitoring configuration parameters
#[derive(Debug, Clone)]
pub struct HttpMonitorConfig {
pub url: String, // Define the URL for HTTP monitoring
pub method: HttpMethodTypes, // GET, POST, etc.
pub timeout: u64, // Configure timeout
pub headers: Option<HeaderMap<HeaderValue>>, // Optional HTTP headers required for the request URL
pub body: Option<HttpBody>, // Optional body required for the request URL
pub rules: Option<Vec<ContentVerificationRulesSingle>>, // Configurable monitoring rules
}
// Here we have simply implemented the monitoring rules
// Define the content monitoring rule struct detail fields
#[derive(Debug, Clone, Default, serde::Deserialize)]
pub struct ContentVerificationRulesSingle {
pub rule_type: ContentVerificationRules, // Only supports Contains, NotContains, Regex three different rules
pub rule_content: String, // The keyword for each rule
pub rule_description: String, // The description field for the rule
}
1.2 Check Result CheckResult
// Define the check result struct
#[derive(Debug, Clone)]
pub struct CheckResult {
pub id: u128, // Unique identifier for the monitoring record
pub monitor_type: MonitorType, // Monitoring type
pub target: Option<String>, // The target URL or IP address to monitor
pub status: bool, // Monitoring status, true indicates normal, false indicates abnormal
pub details: CheckResultDetail, // Detailed information of the monitoring result, still different for each monitoring type
}
// Define different types of monitoring return result detail struct
#[derive(Debug, Clone)]
pub enum CheckResultDetail {
Http(HttpMonitorResult),
Https(HttpsMonitorResult),
Ftp(FtpMonitorResult),
Traceroute(TracerouteMonitorResult),
Cpu(CpuMonitorResult),
Memory(MemoryMonitorResult),
Disk(DiskMonitorResult),
Process(ProcessMonitorResult),
Icmp(IcmpMonitorResult),
Tcp(TcpMonitorResult),
Udp(UdpMonitorResult),
Dns(DnsMonitorResult),
Unknown(UnknownQueryResult),
}
// Define a struct to save the final result parameters of HTTP monitoring
#[derive(Debug, Clone, Default)]
pub struct HttpMonitorResult{
pub basic_avaliable: BasicAvailability, // Contains basic detection content
pub response_headers: SecurityHeaders, // Security-related header information
pub performance_timings: PerformanceTimings, // Performance metrics related information
pub certificate_info: CertificateInfo, // SSL certificate information
pub content_verification: ContentVerificationResult, // Content detection related information
pub advanced_avaliable: AdvancedAvailability, // Advanced monitoring related information
}
// Here we will not display the specific implementation of various types of detection in HTTP monitoring; we will explain each type of detection content in detail later.
Through the previous explanation, we have completed the definition of the public Trait and the declaration of related parameters, laying the core foundation of the framework. Next, we will enter the implementation part of the monitoring engine factory.
First, let’s clarify the core responsibilities of the factory:The monitoring engine factory is used to dynamically create corresponding monitoring engine instances based on different monitoring types. For example, when I need to monitor an HTTP service, I just need to tell the factory, “I need an HTTP monitoring engine,” and the factory will return an engine instance specifically for executing HTTP checks.
Understanding this core concept makes the specific code implementation clear and straightforward:
src/monitor/mod.rs
pub mod types;
use crate::tools_types::MonitorType; // Global common type definition module
// Introduce trait objects
pub mod monitor_trait;
use monitor_trait::Monitor;
// Declare specific monitoring engines
mod icmp_monitor;
mod tcp_monitor;
mod udp_monitor;
mod dns_monitor;
mod http_monitor;
mod ftp_monitor;
mod traceroute_monitor;
mod cpu_monitor;
mod memory_monitor;
mod disk_monitor;
mod process_monitor;
// Different types of monitoring systems
use icmp_monitor::IcmpMonitor;
use tcp_monitor::TcpMonitor;
use udp_monitor::UdpMonitor;
use dns_monitor::DnsMonitor;
use http_monitor::HttpMonitor;
use ftp_monitor::FtpMonitor;
use traceroute_monitor::TracerouteMonitor;
use cpu_monitor::CpuMonitor;
use memory_monitor::MemoryMonitor;
use disk_monitor::DiskMonitor;
use process_monitor::ProcessMonitor;
// Define the monitoring factory
pub struct MonitorFactory;
impl MonitorFactory {
// Based on the strategy pattern, the factory function returns a monitoring engine that implements the Monitor trait
// The input parameter is the monitoring type, and the output parameter is the specific monitoring engine
// Return type is unified through Box<>, and since the size is unknown, it must be placed after Box, and polymorphism is called through vTable, so the caller does not need to care about the specific type
pub fn create_monitor(monitor_type: MonitorType) -> Box<dyn Monitor> {
match monitor_type {
MonitorType::Icmp => Box::new(IcmpMonitor::new()),
MonitorType::Tcp => Box::new(TcpMonitor::new()),
MonitorType::Udp => Box::new(UdpMonitor::new()),
MonitorType::Dns => Box::new(DnsMonitor::new()),
MonitorType::Http => Box::new(HttpMonitor::new()),
MonitorType::Ftp => Box::new(FtpMonitor::new()),
MonitorType::Traceroute => Box::new(TracerouteMonitor::new()),
MonitorType::Cpu => Box::new(CpuMonitor::new()),
MonitorType::Memory => Box::new(MemoryMonitor::new()),
MonitorType::Disk => Box::new(DiskMonitor::new()),
MonitorType::Process => Box::new(ProcessMonitor::new()),
}
}
}
Next, we will define the specific monitoring engine modules. All monitoring engines follow the same structure: implementing the unified <span>Monitor</span> trait and perfecting the <span>check</span> method to carry the specific logic of various monitoring tasks. For ease of understanding, we will only display one monitoring type as an example; the implementation methods for other types are similar.
// src/monitor/cpu_monitor.rs
use super::monitor_trait::Monitor;
use super::types::{ MonitorConfig, CheckResult, CheckResultDetail, CpuMonitorResult};
use crate::tools_types::MonitorType;
pub struct CpuMonitor {
}
impl CpuMonitor {
pub fn new() -> Self {
CpuMonitor {}
}
}
// To make the async methods in the trait usable, please refer to Q&A for details
#[async_trait::async_trait]
impl Monitor for CpuMonitor {
async fn check(&self, config: &MonitorConfig) -> CheckResult {
CheckResult {
id: uuid::Uuid::new_v4().as_u128(),
monitor_type: MonitorType::Cpu,
target: config.target.clone(),
status: true,
details: CheckResultDetail::Cpu(CpuMonitorResult::default())
}
}
fn get_type(&self) -> MonitorType {
MonitorType::Cpu
}
}
After completing the declaration and module definition of the multi-type monitoring engine, our various engines are now capable of running independently. However, the engines need to be coordinated and driven, which is the core role of the scheduler.
The scheduler, as the hub of the entire system, is responsible for linking the complete monitoring process: it first obtains the list of monitoring tasks, and then dynamically calls the corresponding monitoring engine to execute monitoring checks based on the specific configuration parameters of each task. In short, the scheduler does not care about how to monitor specifically, but focuses on <span>"when"</span> and <span>"how to schedule"</span> monitoring tasks.
Next, we will specifically implement this key monitoring scheduler:
2. Specific Implementation of the Monitoring Engine Scheduler
Although the scheduler bears the title of the system’s “hub,” its core responsibilities are indeed clear and focused—there is no need to care about the specific detection logic, just focus on the scheduling itself. Specifically, it needs to accomplish three core tasks:
- Asynchronous Scheduling: Concurrently call the detection methods of each monitoring engine;
- Loop Execution: Start polling tasks at preset intervals to achieve continuous monitoring;
- Result Collection: Summarize and output the execution results of each task.
Now, let’s implement this scheduling logic:
use std::{ time::Duration}; // Import the time module
// Define a common enum value module so that it can be used elsewhere through use crate::tools_types..
pub mod tools_types;
use tools_types::{ convert_to_monitor_config };
// Asynchronous monitoring module, used to implement asynchronous scheduling
mod async_monitor;
use async_monitor::AsyncMonitor;
// Define a file reading module, encapsulating common file processing operations
mod tools;
use tools::file_tool::{read_json_file};
// Define the monitoring engine module, introducing the monitoring engine factory object
pub mod monitor;
use monitor::{ MonitorFactory } ;
#[tokio::main]
async fn main() {
println!("Network Monitor Starting...");
// Get the list of websites to monitor; here we changed it to a JSON file, where each monitoring engine parameter is an object, making it easy to configure through the page later, similar to the following data structure:
// [{
// "target": "https://www.google.com",
// "monitor_type": "HTTP",
// "method": "GET",
// "params": {},
// "headers": {},
// "rules": [
// {
// "rule_type": "contains",
// "rule_content": "Google",
// "rule_description": "Check if the page contains the string 'Google'"
// }
// ],
// "timeout": 5000
// }]
let monitor_website_list = read_json_file("monitor_list.json");
match monitor_website_list {
Ok(monitor_list) => {
// Store the running results of the threads
let mut result_monitors = Vec::new();
for monitor in monitor_list {
// Create a specific monitor instance
let target = MonitorFactory::create_monitor(monitor.monitor_type);
// Convert the object in the monitoring configuration file into the object needed for the monitor check
let monitor_config = convert_to_monitor_config(&monitor);
// If it is a polling monitor, create a polling monitor
match monitor.interval {
Some(interval) => {
// Create a polling monitoring scheduler
let async_monitor = AsyncMonitor::create_interval_monitoring(target, monitor_config).await;
result_monitors.push(async_monitor);
},
None => {
// Create a single monitoring scheduler
let once_monitor = AsyncMonitor::create_once_monitoring(target, monitor_config).await;
result_monitors.push(once_monitor);
}
}
}
// Process all monitoring results
for monitor_receiver in result_monitors {
// Use tokio::spawn asynchronous tasks to monitor different types of schedulers, avoiding blocking the main process
tokio::spawn(async move {
// Here, use a loop to receive all monitoring results
let mut result_receiver = monitor_receiver;
while let Some(result) = result_receiver.recv().await {
// Directly print the relevant detection results; later we will connect to the H5 page to implement
println!("Received result: {:?}", result);
}
});
}
println!("All monitors have started, the program will continue running");
// You can adjust this time as needed or use other methods to keep the program running
tokio::time::sleep(Duration::from_secs(60)).await; // Let the program run for 60 seconds, then it will exit
},
Err(err) => {
eprintln!("Error reading monitor list: {}", err.to_string());
}
}
println!("All URL checks completed");
}
Among the above, we used polling schedulers and single-time schedulers. Next, let’s look at the relevant implementation logic:
src/async_monitor.rs
use tokio::time::{interval, Duration};
use crate::monitor::monitor_trait::Monitor;
use crate::monitor::types::{MonitorConfig, CheckResult};
// 1. Introduce the mpsc channel concept to create a channel and return the receiver to the caller
use tokio::sync::mpsc;
use std::pin::Pin;
// Here we uniformly return the type of asynchronous monitoring, which is convenient for subsequent unified monitoring result detection
type MonitorResultReceiver = mpsc::Receiver<CheckResult>;
type PinnedReceiverFuture = Pin<Box<dyn Future<Output = MonitorResultReceiver> + Send>;
pub struct AsyncMonitor {
}
impl AsyncMonitor {
// Create a polling monitor
pub fn create_interval_monitoring(target: Box<dyn Monitor>, config: MonitorConfig) -> PinnedReceiverFuture {
Box::pin(async move {
let (tx, rx) = mpsc::channel(100);
tokio::spawn(async move {
let mut interval: tokio::time::Interval = interval(Duration::from_secs(config.interval.unwrap()));
loop {
interval.tick().await;
let check_result = target.check(&config).await;
if tx.send(check_result).await.is_err() {
println!("Error sending result to channel");
}
}
});
// Return the receiver object
rx
})
}
// Create a single monitoring
pub fn create_once_monitoring( target: Box<dyn Monitor>, config: MonitorConfig) -> PinnedReceiverFuture {
Box::pin(async move {
let (tx, rx) = mpsc::channel(1);
tokio::spawn(async move {
let check_result = target.check(&config).await;
if tx.send(check_result).await.is_err() {
println!("Error sending result to channel");
}
});
// Return the receiver object
rx
})
}
}
🎉 Congratulations! 🎉
If you have followed along to this point, congratulations—you have successfully completed the construction of the core framework of the entire monitoring system! This is a significant milestone worthy of applause! 👏
According to our initial plan, we will now enter the implementation phase of specific monitoring types. In this implementation, we will focus on the detailed logic development of HTTP monitoring, which is also the most commonly used and basic service monitoring type.
As for other monitoring types (such as TCP, DNS, ICMP, etc.), we will leave them as after-class practice assignments— of course, I do not rule out the possibility of quietly updating and supplementing them later 🙄
Implementing the HTTP/HTTPS Monitoring Engine
Based on the previous requirement analysis, we have clarified the functions that need to be implemented for HTTP monitoring. Now, we will start defining the corresponding monitoring struct according to the established design.
1.1 Defining the HTTP Monitoring Type Struct
// src/tools_types.rs
// Define a struct to save the final result parameters of HTTP-related monitoring
#[derive(Debug, Clone, Default)]
pub struct HttpMonitorResult{
pub basic_avaliable: BasicAvailability,
pub response_headers: SecurityHeaders,
pub performance_timings: PerformanceTimings,
pub certificate_info: CertificateInfo, // SSL certificate information
pub content_verification: ContentVerificationResult,
pub advanced_avaliable: AdvancedAvailability,
}
In the above, we have already seen the final monitoring results of the HTTP monitoring engine, which includes six modules:
<span>BasicAvailability</span>: Basic monitoring results, including accessibility, status code, protocol type, etc.
// Define a struct to save basic HTTP availability information
#[derive(Debug, Clone, Default)]
pub struct BasicAvailability{
pub is_reachable: bool, // Is it reachable
pub dns_resolvable: bool, // Is DNS resolution successful
pub tcp_connect_success: bool, // Is TCP connection successful
// HTTP related
pub res_received: bool, // Is a response received
pub res_status_code: Option<u16>, // Response status code
pub res_status_category: StatusCategory, // Response status code category
pub protocol_version: String, // HTTP protocol version (e.g., HTTP/1.1, HTTP/2)
pub res_content_type: Option<String>, // Response content type
pub res_content_length: u64, // Response content length
pub res_charset: Option<String>, // Response character set
}
<span>SecurityHeaders</span>: Security header-related monitoring results, whether some security header information is included:<span>strict_transport_security, content_security_policy</span>, etc.
// Define a struct to save security header monitoring information
#[derive(Debug, Clone, Default)]
pub struct SecurityHeaders{
pub strict_transport_security: Option<String>, // Is there a Strict-Transport-Security header
pub content_security_policy: Option<String>, // Is there a Content-Security-Policy header
pub x_content_type_options: Option<String>, // Is there an X-Content-Type-Options header
pub x_frame_options: Option<String>, // Is there an X-Frame-Options header
pub x_xss_protection: Option<String>, // Is there an X-XSS-Protection header
pub referrer_policy: Option<String>, // Is there a Referrer-Policy header
pub feature_policy: Option<String>, // Is there a Feature-Policy header
pub permissions_policy: Option<String>, // Is there a Permissions-Policy header
pub security_headers_ok: bool, // Do all security headers exist
}
<span>PerformanceTimings</span>: Performance metrics, including TCP connection time, TLS handshake time, connection duration, etc.
// Create an enum type representing the categories of performance metrics
#[derive(Debug, Clone, Default)]
pub struct PerformanceTimings {
// Performance metrics related
pub dns_lookup_time: u128, // DNS query time, in milliseconds
pub tcp_connect_time: u128, // TCP connection time, in milliseconds
pub tls_handshake_time: u128, // TLS handshake time, in milliseconds
pub first_byte_time: u128, // First byte time, in milliseconds
pub content_download_time: u128, // Content download time, in milliseconds
pub ssl_negotiation_time: u128, // SSL negotiation time, in milliseconds
pub ssl_cert_valid: bool, // Is the SSL certificate valid
pub request_sending_time: u128, // Request sending time, in milliseconds
pub server_processing_time: u128, // Server processing time, in milliseconds
pub response_receiving_time: u128, // Response receiving time, in milliseconds
pub total_time: u128, // Total time, in milliseconds
}
<span>CertificateInfo</span>: SSL certificate-related content, including certificate number, validity, etc.
#[derive(Debug, Clone, Default)]
pub struct CertificateInfo {
pub issuer: Option<String>, // Certificate issuer
pub subject: Option<String>, // Certificate subject
pub valid_from: Option<String>, // Certificate validity start time
pub valid_until: Option<String>, // Certificate validity end time
pub serial_number: Option<String>, // Certificate serial number
pub signature_algorithm: Option<String>, // Signature algorithm
pub public_key_algorithm: Option<String>, // Public key algorithm
pub public_key_size: Option<usize>, // Public key size
pub is_valid: bool, // Is the certificate valid
}
<span>ContentVerificationResult</span>: Response content monitoring results, including whether certain fields are included, regex matching content, etc.
// Create a content verification result
#[derive(Debug, Clone, Default)]
pub struct ContentVerificationResult {
pub match_rules: Vec<ContentVerificationRulesResult>, // Matched rules
pub failed_rules: Vec<ContentVerificationRulesResult>, // Unmatched rules and their reasons
}
// Define the core content verification return data structure
#[derive(Debug, Clone, Default)]
pub struct ContentVerificationRulesResult {
pub rule_id: u64,
pub status:StatusInfo,
pub message: String,
pub rules: ContentVerificationRulesSingle
}
// Define content monitoring rule struct detail fields
#[derive(Debug, Clone, Default, serde::Deserialize)]
pub struct ContentVerificationRulesSingle {
pub rule_type: ContentVerificationRules,
pub rule_content: String,
pub rule_description: String,
}
// Content verification categories
#[derive(Debug, Clone, serde::Deserialize)]
pub enum ContentVerificationRules {
#[serde(rename = "contains")]
Contains, // Contains specified string in response content
#[serde(rename = "not_contains")]
NotContains, // Does not contain specified string in response content
#[serde(rename = "regex")]
Regex, // Matches regular expression in response content
// Define a default value for matching
#[serde(rename = "default")] // Define default value
Default,
}
// Set default value
impl Default for ContentVerificationRules {
fn default() -> Self {
ContentVerificationRules::Default
}
}
#[derive(Debug, Clone, serde::Deserialize)]
pub enum StatusInfo {
Success,
Failed,
Unknown,
}
impl Default for StatusInfo {
fn default() -> Self {
StatusInfo::Unknown
}
}
- AdvancedAvailability: Advanced monitoring content, including transaction monitoring, etc., which will not be covered this time…
// Advanced availability category business metrics monitoring transaction monitoring, etc.
#[derive(Debug, Clone, Default)]
pub struct AdvancedAvailability {
pub bussiness_metrics: HashMap<String, String>,
}
Now, we have completed the definition of the monitoring result struct. Next, we need to gradually fill in the values of each field—this is like completing the last piece of a puzzle. When all fields are correctly assigned, the implementation of the entire HTTP monitoring module will be successfully completed.
Let’s immediately start this final implementation phase and inject actual data content into each field.
1.2 Implementation of the HTTP Monitoring Engine
Now, let’s turn our attention back to the initially designed HTTP monitoring engine. In the previous framework construction, we have implemented a basic result return logic. Next, we will deepen this based on that and fill in more actual functions.
As mentioned earlier, we will continue to use the <span>reqwest</span> library to handle HTTP requests. Its basic usage will not be repeated here; now let’s focus on further improving the complete logic of the <span>HttpMonitor</span> engine.
src/monitor/http_monitor.rs
pub struct HttpMonitor {
client: Client,
}
impl HttpMonitor {
pub fn new() -> Self {
// Create a Client for subsequent URL calls
HttpMonitor {
client: Client::builder()
.redirect(reqwest::redirect::Policy::limited(5)) // Limit redirection to 5 times
.build().expect("Failed to create HTTP client"),
}
}
// Get an HTTP client link based on the method to determine how to create an HTTP link
pub fn get_client(&self, config: &HttpMonitorConfig) -> reqwest::RequestBuilder {
// Create an HTTP request
let mut request_client = match config.method {
HttpMethodTypes::Get => self.client.get(config.url.as_str()),
HttpMethodTypes::Post => self.client.post(config.url.as_str()),
HttpMethodTypes::Put => self.client.put(config.url.as_str()),
HttpMethodTypes::Delete => self.client.delete(config.url.as_str()),
HttpMethodTypes::Head => self.client.head(config.url.as_str()),
HttpMethodTypes::Patch => self.client.patch(config.url.as_str()),
_ => self.client.get(config.url.as_str()), // Default to GET method
};
// Set timeout based on configuration information
request_client = request_client.timeout(std::time::Duration::from_millis(config.timeout));
// If the user has customized header information, add it here
if let Some(headers) = &config.headers {
request_client = request_client.headers(headers.clone());
}
// Compatible with several different body types, set them accordingly
if let Some(body) = &config.body {
match body {
HttpBody::Text(text) => {
request_client = request_client.body(text.clone());
},
HttpBody::Binary(bin) => {
request_client = request_client.body(bin.clone());
},
HttpBody::Json(json_value) => {
request_client = request_client.json(json_value);
},
HttpBody::Empty => {request_client = request_client.body("");},
}
}
// Finally return an HTTP connection Client
request_client
}
// Create a function to generate basic monitoring results
fn create_basic_result(&self, ...) -> BasicAvailability {🚗🚗🚗🚗🚗🚗🚗}
// Create a function to generate security header monitoring results
fn create_headers_result(&self,...) -> SecurityHeaders{🚗🚗🚗🚗🚗🚗🚗}
// Create a function to generate performance metrics monitoring results
fn create_performance_timings(&self,...) -> PerformanceTimings {🚗🚗🚗🚗🚗🚗🚗}
// Create a function to verify response content monitoring results
fn create_content_verification_result(&self,...) -> ContentVerificationResult{🚗🚗🚗🚗🚗🚗🚗}
// Create advanced availability results, directly return a default result
fn create_advanced_availability_result(&self) -> AdvancedAvailability {
AdvancedAvailability {
bussiness_metrics: HashMap::new(),
}
}
}
// Below is the implementation of the specific check method
#[async_trait::async_trait]
impl Monitor for HttpMonitor{
async fn check(&self, config: &MonitorConfig) -> CheckResult {
// Here is our main battlefield. First, let’s write the exception cases, and then we will gradually fill in the details.
// Check if there is a target; at this point, the target should be a URL address
if config.target.is_none() {
return CheckResult {
id: uuid::Uuid::new_v4().as_u128(), // V4 uuid is a random number based on timestamp and random number, V4 version generation function
monitor_type: MonitorType::Http,
target: None,
status: false, // Monitoring status failed, as it never entered monitoring
details: CheckResultDetail::Http(HttpMonitorResult::default()), // default indicates calling the Default trait method, default value for each field is the default value
};
}
// Handle specific HTTP monitoring type data
match config.details {
MonitorConfigDetail::Http(ref detail) => {
// Here, as we saw in the previous chapter, we first need to send an HTTP request to the Target address
let request_client = self.get_client(detail);
// Send the request to match the returned result
match request_client.send().await{
Ok(response)=> {
// In fact, what we need to do is to gradually improve the logic here. If you are attentive to this point, don’t rush; the content here will be explained below~~~
🛺🛺🛺🛺🛺🛺🛺🛺 Get ready to set off!!!! 🛺🛺🛺🛺🛺🛺🛺🛺
},
Err(e) => {
// Check various error types
return CheckResult {
id: uuid::Uuid::new_v4().as_u128(), // V4 uuid is a random number based on timestamp and random number, V4 version generation function
monitor_type: MonitorType::Http,
target: config.target.clone(),
status: true, // Indicates that the monitoring status is successful, but the target access failed
details: CheckResultDetail::Http(HttpMonitorResult::default()),
};
},
}
},
_ => {
// If it is not an HTTP monitoring type, return an error result
return CheckResult {
id: uuid::Uuid::new_v4().as_u128(), // V4 uuid is a random number based on timestamp and random number, V4 version generation function
monitor_type: MonitorType::Http,
target: None,
status: false, // Monitoring status failed, as it never entered monitoring
details: CheckResultDetail::Unknown(UnknownQueryResult {
description: "Calling monitoring type: HTTP, please verify before continuing".to_string(),
query_type: MonitorType::Http,
}),
};
},
}
}
// Define a function to return the specific type
fn get_type(&self) -> MonitorType {
MonitorType::Http
}
}
If you can persist in learning to this point, congratulations—our code implementation part has been successfully completed!
Looking back at the entire development process, we have actually been engaged in a clever “fill-in-the-blank game”: first building the overall framework structure, and then gradually filling in the specific implementations of each module. This macro-to-micro development approach allows us to maintain a clear mindset, ensuring we do not lose direction even when facing complex systems.
In this process, my own development experience has been similar—from initially being unfamiliar with each module to gradually breaking down requirements, studying technical details, and finally independently completing the entire function implementation. This experience has made me deeply realize that for beginners, hands-on practice is crucial.
Thus, we conclude this phase of implementation. I suggest everyone take the time to digest and absorb this content and try to expand more monitoring types on your own. I look forward to meeting everyone again in the upcoming learning journey!
Process Issues & Solutions
<span>#[async_trait::async_trait]</span>What is the purpose of this macro? Answer: It is the async-trait macro, used to make async methods in traits usable. Key points:
- In Rust’s native
<span>trait</span>, writing<span>async fn (or returning impl Future) when used as a trait object (like Box<dyn Monitor>) does not satisfy object safety and will not compile.</span> <span>#[async_trait::async_trait]</span>will automatically expand your<span>async fn</span>into a form that returns<span>Pin<Box<dyn Future<Output = *> + Send + '*>></span>, allowing the trait to be used as<span>dyn Trait</span>.
// Original writing
#[async_trait::async_trait]
trait Monitor {
async fn check(&self, cfg: &MonitorConfig) -> CheckResult;
}
// Expanded version is roughly equivalent
trait Monitor {
fn check<'a>(&'a self, cfg: &'a MonitorConfig)
-> core::pin::Pin<Box<dyn core::future::Future<Output = CheckResult> + Send + 'a>>;
}
- In
<span>src/async_monitor.rs</span>, using<span>mpsc</span>,<span>Box::pin</span>, and<span>tokio::spawn</span>what are they used for? Answer: To turn an async block into a “returnable/storable” Future, to unify the return type and fix it on the heap (Pin), making it easy to await and store in collections. Key points explained:
<span>async move {...}</span>: Generates a<span>Future</span>and moves the ownership of<span>target, config</span>into it, ensuring it meets the<span>'static</span>requirement in<span>tokio::spawn</span>.<span>Box::pin(...):</span>Places this<span>Future</span>on the heap and pins its location (Pin), then does type erasure through<span>dyn Future</span>to obtain a unified return type<span>PinnedReceiverFuture = Pin<Box<dyn Future<Output = mpsc::Receiver<_>> + Send>></span><span>.</span><span>mpsc channel</span>: The function creates<span>tx/r</span>x, and the background loop uses<span>check</span>to send results each time; the function itself returns<span>rx</span>(obtained through<span>await</span>).
Final Thoughts
Indeed, this content was initially positioned as an enterprise-level monitoring system, but during the actual development process, I deeply realized what it means to be “ignorant and fearless.” To truly build a monitoring system that can be called “enterprise-level,” the dimensions to consider far exceed expectations.
In my view, the seemingly invisible gap between “usable” and “functioning” is fundamentally determined by user scale and financial investment. When the user base reaches a certain scale, those details that seem insignificant during the demo phase—such as performance bottlenecks, data consistency, and high availability architecture—will suddenly become fatal gaps in system stability. Once financial flows or business losses are involved, the reliability requirements for the system will leap to a new level.
Therefore, many times we think that a “simple” system or function is not really simple; it just has not yet undergone the test of large-scale users and real funds. What we have implemented this time is merely a basic HTTP monitoring module, and there are still many functions to be improved to reach a complete enterprise-level solution.
However, even so, for a beginner, being able to walk through this process from zero to one, understanding the core architecture and implementation logic of the monitoring system, is already a highly valuable growth experience. May this practical experience become a solid first step for you towards more complex system design.
In today’s world where AI technology is so prevalent, I want to share a piece of advice: please do not overly rely on AI’s code generation capabilities. Excellent developers should view AI as a tool to enhance efficiency, rather than a replacement for thought. It is important to maintain your own thought leadership—you should control the technology, not be controlled by it.