use std::{collections::HashMap, str::FromStr};
use serde::Serialize;
use serde_json::to_string_pretty;
use smallvec::smallvec;
use crate::{
http::{
headers::{ContentType, HeaderEntry, HttpHeader, HttpHeaderValue},
requests::HttpRequest,
responses::HttpResponse,
status::HttpStatus,
},
utils::buffer::Buffer,
};
#[derive(Debug, Serialize)]
pub struct MetricsHandler {
pub server_name: String,
pub metrics_endpoint: String,
pub requests_count: u32,
pub status_codes: HashMap<u16, u32>,
}
impl MetricsHandler {
pub fn new(server_name: String, metrics_endpoint: String) -> Self {
Self {
server_name,
metrics_endpoint,
requests_count: 0,
status_codes: HashMap::new(),
}
}
pub fn metrics(&self) -> HttpResponse {
let body = to_string_pretty(self)
.map(|json| Buffer::from_str(&json).unwrap())
.ok();
let body_length = body.as_ref().map(|buf| buf.len()).unwrap_or(0) as u64;
let content_length = HeaderEntry::new(
HttpHeader::ContentLength,
HttpHeaderValue::ContentLength(body_length),
);
let content_type = HeaderEntry::new(
HttpHeader::ContentType,
HttpHeaderValue::ContentType(ContentType::ApplicationJson),
);
let headers = smallvec![content_length, content_type];
HttpResponse::new((1, 1), HttpStatus::Ok, headers, body, None)
}
pub fn add_metrics(&mut self, _: &HttpRequest, response: &HttpResponse) {
self.requests_count += 1;
self.status_codes
.entry(response.status as u16)
.and_modify(|cpt| *cpt += 1)
.or_insert(1);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::http::methods::HttpMethod;
use crate::http::responses::HttpResponse;
use crate::http::status::HttpStatus;
#[test]
fn test_add_metrics_increments_counts() {
let mut metrics = MetricsHandler::new("TestServer".into(), "/metrics".into());
let resp_200 = dummy_response(HttpStatus::Ok);
let resp_404 = dummy_response(HttpStatus::NotFound);
metrics.add_metrics(&dummy_request(), &resp_200);
metrics.add_metrics(&dummy_request(), &resp_404);
metrics.add_metrics(&dummy_request(), &resp_200);
assert_eq!(metrics.requests_count, 3);
assert_eq!(metrics.status_codes.get(&(HttpStatus::Ok as u16)), Some(&2));
assert_eq!(
metrics.status_codes.get(&(HttpStatus::NotFound as u16)),
Some(&1)
);
}
#[test]
fn test_metrics_endpoint_response() {
let metrics = MetricsHandler::new("server".to_owned(), "/metrics".to_owned());
let response = metrics.metrics();
assert_eq!(response.status, HttpStatus::Ok);
let content_type_header = response
.headers
.iter()
.find(|entry| matches!(entry.name, HttpHeader::ContentType));
assert!(content_type_header.is_some());
if let Some(header) = content_type_header {
if let HttpHeaderValue::ContentType(ContentType::ApplicationJson) = header.value {
} else {
panic!("Content-Type is not application/json");
}
} else {
panic!("Missing Content-Type header");
}
assert!(response.body.is_some());
}
fn dummy_request() -> HttpRequest {
HttpRequest::new(HttpMethod::GET, "/".to_owned(), (1, 1), smallvec![], None)
}
fn dummy_response(status: HttpStatus) -> HttpResponse {
HttpResponse::new((1, 1), status, smallvec![], None, None)
}
}