✨ Metrics
This commit is contained in:
@@ -1,7 +1,6 @@
|
||||
use std::sync::RwLock;
|
||||
|
||||
use config::Config;
|
||||
use lazy_static::lazy_static;
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
use crate::config::loader::load_settings;
|
||||
|
||||
|
33
src/main.rs
33
src/main.rs
@@ -2,11 +2,28 @@ mod config;
|
||||
mod proxies;
|
||||
mod sideload;
|
||||
|
||||
use poem::{listener::TcpListener, EndpointExt, Route, Server};
|
||||
use std::collections::VecDeque;
|
||||
|
||||
use lazy_static::lazy_static;
|
||||
use poem::{listener::TcpListener, Route, Server};
|
||||
use poem_openapi::OpenApiService;
|
||||
use proxies::RoadInstance;
|
||||
use tokio::sync::Mutex;
|
||||
use tracing::{error, info, Level};
|
||||
|
||||
use crate::proxies::route;
|
||||
use crate::proxies::{metrics::RoadMetrics, route};
|
||||
|
||||
lazy_static! {
|
||||
static ref ROAD: Mutex<RoadInstance> = Mutex::new(RoadInstance {
|
||||
regions: vec![],
|
||||
metrics: RoadMetrics {
|
||||
requests_count: 0,
|
||||
failures_count: 0,
|
||||
recent_successes: VecDeque::new(),
|
||||
recent_errors: VecDeque::new(),
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), std::io::Error> {
|
||||
@@ -19,19 +36,17 @@ async fn main() -> Result<(), std::io::Error> {
|
||||
.init();
|
||||
|
||||
// Prepare all the stuff
|
||||
let mut instance = proxies::Instance::new();
|
||||
|
||||
info!("Loading proxy regions...");
|
||||
match proxies::loader::scan_regions(
|
||||
config::C
|
||||
.read()
|
||||
.unwrap()
|
||||
.await
|
||||
.get_string("regions")
|
||||
.unwrap_or("./regions".to_string()),
|
||||
) {
|
||||
Err(_) => error!("Loading proxy regions... failed"),
|
||||
Ok((regions, count)) => {
|
||||
instance.regions = regions;
|
||||
ROAD.lock().await.regions = regions;
|
||||
info!(count, "Loading proxy regions... done")
|
||||
}
|
||||
};
|
||||
@@ -40,11 +55,11 @@ async fn main() -> Result<(), std::io::Error> {
|
||||
let proxies_server = Server::new(TcpListener::bind(
|
||||
config::C
|
||||
.read()
|
||||
.unwrap()
|
||||
.await
|
||||
.get_string("listen.proxies")
|
||||
.unwrap_or("0.0.0.0:80".to_string()),
|
||||
))
|
||||
.run(route::handle.data(instance));
|
||||
.run(route::handle);
|
||||
|
||||
// Sideload
|
||||
let sideload = OpenApiService::new(sideload::SideloadApi, "Sideload API", "1.0")
|
||||
@@ -54,7 +69,7 @@ async fn main() -> Result<(), std::io::Error> {
|
||||
let sideload_server = Server::new(TcpListener::bind(
|
||||
config::C
|
||||
.read()
|
||||
.unwrap()
|
||||
.await
|
||||
.get_string("listen.sideload")
|
||||
.unwrap_or("0.0.0.0:81".to_string()),
|
||||
))
|
||||
|
80
src/proxies/metrics.rs
Normal file
80
src/proxies/metrics.rs
Normal file
@@ -0,0 +1,80 @@
|
||||
use std::collections::VecDeque;
|
||||
|
||||
use poem_openapi::Object;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::config::{Destination, Location, Region};
|
||||
|
||||
#[derive(Debug, Object, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct RoadTrace {
|
||||
pub region: String,
|
||||
pub location: String,
|
||||
pub destination: String,
|
||||
pub error: Option<String>,
|
||||
}
|
||||
|
||||
impl RoadTrace {
|
||||
pub fn from_structs(reg: Region, loc: Location, end: Destination) -> RoadTrace {
|
||||
RoadTrace {
|
||||
region: reg.id,
|
||||
location: loc.id,
|
||||
destination: end.id,
|
||||
error: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_structs_with_error(
|
||||
reg: Region,
|
||||
loc: Location,
|
||||
end: Destination,
|
||||
err: String,
|
||||
) -> RoadTrace {
|
||||
RoadTrace {
|
||||
region: reg.id,
|
||||
location: loc.id,
|
||||
destination: end.id,
|
||||
error: Some(err),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RoadMetrics {
|
||||
pub requests_count: u64,
|
||||
pub failures_count: u64,
|
||||
|
||||
pub recent_successes: VecDeque<RoadTrace>,
|
||||
pub recent_errors: VecDeque<RoadTrace>,
|
||||
}
|
||||
|
||||
impl RoadMetrics {
|
||||
pub fn get_success_rate(&self) -> f64 {
|
||||
if self.requests_count > 0 {
|
||||
(self.requests_count - self.failures_count) as f64 / self.requests_count as f64
|
||||
} else {
|
||||
0.0
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_success_request(&mut self, reg: Region, loc: Location, end: Destination) {
|
||||
self.requests_count += 1;
|
||||
self.recent_successes
|
||||
.push_back(RoadTrace::from_structs(reg, loc, end));
|
||||
}
|
||||
|
||||
pub fn add_faliure_request(
|
||||
&mut self,
|
||||
reg: Region,
|
||||
loc: Location,
|
||||
end: Destination,
|
||||
err: String, // For some reason error is rarely clonable, so we use preformatted message
|
||||
) {
|
||||
self.requests_count += 1;
|
||||
self.failures_count += 1;
|
||||
self.recent_errors
|
||||
.push_back(RoadTrace::from_structs_with_error(reg, loc, end, err));
|
||||
if self.recent_errors.len() > 10 {
|
||||
self.recent_errors.pop_front();
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,30 +1,35 @@
|
||||
use http::Method;
|
||||
use poem::http::{HeaderMap, Uri};
|
||||
use regex::Regex;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use wildmatch::WildMatch;
|
||||
|
||||
use self::config::{Location, Region};
|
||||
use self::{
|
||||
config::{Location, Region},
|
||||
metrics::RoadMetrics,
|
||||
};
|
||||
|
||||
pub mod browser;
|
||||
pub mod config;
|
||||
pub mod loader;
|
||||
pub mod metrics;
|
||||
pub mod responder;
|
||||
pub mod route;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Instance {
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RoadInstance {
|
||||
pub regions: Vec<Region>,
|
||||
pub metrics: RoadMetrics,
|
||||
}
|
||||
|
||||
impl Instance {
|
||||
pub fn new() -> Instance {
|
||||
Instance { regions: vec![] }
|
||||
}
|
||||
|
||||
pub fn filter(&self, uri: &Uri, method: Method, headers: &HeaderMap) -> Option<&Location> {
|
||||
impl RoadInstance {
|
||||
pub fn filter(
|
||||
&self,
|
||||
uri: &Uri,
|
||||
method: Method,
|
||||
headers: &HeaderMap,
|
||||
) -> Option<(&Region, &Location)> {
|
||||
self.regions.iter().find_map(|region| {
|
||||
region.locations.iter().find(|location| {
|
||||
let location = region.locations.iter().find(|location| {
|
||||
let mut hosts = location.hosts.iter();
|
||||
if !hosts.any(|item| {
|
||||
WildMatch::new(item.as_str()).matches(uri.host().unwrap_or("localhost"))
|
||||
@@ -64,7 +69,9 @@ impl Instance {
|
||||
}
|
||||
|
||||
true
|
||||
})
|
||||
});
|
||||
|
||||
location.map(|location| (region, location))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@@ -1,27 +1,30 @@
|
||||
use http::Method;
|
||||
use poem::{
|
||||
handler,
|
||||
http::{HeaderMap, StatusCode, Uri},
|
||||
web::{websocket::WebSocket, Data},
|
||||
web::websocket::WebSocket,
|
||||
Body, Error, FromRequest, IntoResponse, Request, Response, Result,
|
||||
};
|
||||
use rand::seq::SliceRandom;
|
||||
use reqwest::Method;
|
||||
|
||||
use crate::proxies::{
|
||||
config::{Destination, DestinationType},
|
||||
responder,
|
||||
use crate::{
|
||||
proxies::{
|
||||
config::{Destination, DestinationType},
|
||||
responder,
|
||||
},
|
||||
ROAD,
|
||||
};
|
||||
|
||||
#[handler]
|
||||
pub async fn handle(
|
||||
app: Data<&super::Instance>,
|
||||
req: &Request,
|
||||
uri: &Uri,
|
||||
headers: &HeaderMap,
|
||||
method: Method,
|
||||
body: Body,
|
||||
) -> Result<impl IntoResponse, Error> {
|
||||
let location = match app.filter(uri, method.clone(), headers) {
|
||||
let readable_app = ROAD.lock().await;
|
||||
let (region, location) = match readable_app.filter(uri, method.clone(), headers) {
|
||||
Some(val) => val,
|
||||
None => {
|
||||
return Err(Error::from_string(
|
||||
@@ -93,5 +96,27 @@ pub async fn handle(
|
||||
}
|
||||
}
|
||||
|
||||
forward(destination, req, uri, headers, method, body).await
|
||||
let reg = region.clone();
|
||||
let loc = location.clone();
|
||||
let end = destination.clone();
|
||||
|
||||
match forward(&end, req, uri, headers, method, body).await {
|
||||
Ok(resp) => {
|
||||
tokio::spawn(async move {
|
||||
let writable_app = &mut ROAD.lock().await;
|
||||
writable_app.metrics.add_success_request(reg, loc, end);
|
||||
});
|
||||
Ok(resp)
|
||||
}
|
||||
Err(err) => {
|
||||
let message = format!("{:}", err);
|
||||
tokio::spawn(async move {
|
||||
let writable_app = &mut ROAD.lock().await;
|
||||
writable_app
|
||||
.metrics
|
||||
.add_faliure_request(reg, loc, end, message);
|
||||
});
|
||||
Err(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,14 +1,81 @@
|
||||
use poem_openapi::{param::Query, payload::PlainText, OpenApi};
|
||||
use poem_openapi::{payload::Json, ApiResponse, Object, OpenApi};
|
||||
|
||||
use crate::{
|
||||
proxies::{
|
||||
config::{Destination, Location},
|
||||
metrics::RoadTrace,
|
||||
},
|
||||
ROAD,
|
||||
};
|
||||
|
||||
use super::SideloadApi;
|
||||
|
||||
#[derive(ApiResponse)]
|
||||
enum OverviewResponse {
|
||||
/// Return the overview data.
|
||||
#[oai(status = 200)]
|
||||
Ok(Json<OverviewData>),
|
||||
}
|
||||
|
||||
#[derive(Debug, Object, Clone, PartialEq)]
|
||||
struct OverviewData {
|
||||
/// Loaded regions count
|
||||
#[oai(read_only)]
|
||||
regions: usize,
|
||||
/// Loaded locations count
|
||||
#[oai(read_only)]
|
||||
locations: usize,
|
||||
/// Loaded destnations count
|
||||
#[oai(read_only)]
|
||||
destinations: usize,
|
||||
/// Recent requests count
|
||||
requests_count: u64,
|
||||
/// Recent requests success count
|
||||
faliures_count: u64,
|
||||
/// Recent requests falied count
|
||||
successes_count: u64,
|
||||
/// Recent requests success rate
|
||||
success_rate: f64,
|
||||
/// Recent successes
|
||||
recent_successes: Vec<RoadTrace>,
|
||||
/// Recent errors
|
||||
recent_errors: Vec<RoadTrace>,
|
||||
}
|
||||
|
||||
#[OpenApi]
|
||||
impl SideloadApi {
|
||||
#[oai(path = "/hello", method = "get")]
|
||||
async fn index(&self, name: Query<Option<String>>) -> PlainText<String> {
|
||||
match name.0 {
|
||||
Some(name) => PlainText(format!("hello, {name}!")),
|
||||
None => PlainText("hello!".to_string()),
|
||||
}
|
||||
#[oai(path = "/", method = "get")]
|
||||
async fn index(&self) -> OverviewResponse {
|
||||
let locked_app = ROAD.lock().await;
|
||||
let regions = locked_app.regions.clone();
|
||||
let locations = regions
|
||||
.iter()
|
||||
.flat_map(|item| item.locations.clone())
|
||||
.collect::<Vec<Location>>();
|
||||
let destinations = locations
|
||||
.iter()
|
||||
.flat_map(|item| item.destinations.clone())
|
||||
.collect::<Vec<Destination>>();
|
||||
OverviewResponse::Ok(Json(OverviewData {
|
||||
regions: regions.len(),
|
||||
locations: locations.len(),
|
||||
destinations: destinations.len(),
|
||||
requests_count: locked_app.metrics.requests_count,
|
||||
successes_count: locked_app.metrics.requests_count - locked_app.metrics.failures_count,
|
||||
faliures_count: locked_app.metrics.failures_count,
|
||||
success_rate: locked_app.metrics.get_success_rate(),
|
||||
recent_successes: locked_app
|
||||
.metrics
|
||||
.recent_successes
|
||||
.clone()
|
||||
.into_iter()
|
||||
.collect::<Vec<_>>(),
|
||||
recent_errors: locked_app
|
||||
.metrics
|
||||
.recent_errors
|
||||
.clone()
|
||||
.into_iter()
|
||||
.collect::<Vec<_>>(),
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user