feat: add server implementation
Signed-off-by: kjuulh <contact@kjuulh.io>
This commit is contained in:
@@ -1,11 +1,16 @@
|
||||
use std::collections::BTreeMap;
|
||||
use std::time::UNIX_EPOCH;
|
||||
use std::{net::SocketAddr, ops::Deref, sync::Arc};
|
||||
|
||||
use anyhow::Context;
|
||||
use axum::http::Request;
|
||||
use axum::http::{HeaderValue, Method, Request};
|
||||
use axum::routing::get;
|
||||
use axum::Router;
|
||||
use axum::{extract::MatchedPath, response::Html};
|
||||
use axum::{Json, Router};
|
||||
use clap::{Parser, Subcommand};
|
||||
use rand::Rng;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::sync::RwLock;
|
||||
use tower_http::cors::{Any, CorsLayer};
|
||||
use tower_http::trace::TraceLayer;
|
||||
|
||||
#[derive(Parser)]
|
||||
@@ -23,6 +28,15 @@ enum Commands {
|
||||
},
|
||||
}
|
||||
|
||||
const CATEGORIES: [&str; 6] = [
|
||||
"UserOnboarded",
|
||||
"PaymentProcessed",
|
||||
"UserLogin",
|
||||
"UserOffboarded",
|
||||
"OngoingCalls",
|
||||
"CardProcessed",
|
||||
];
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
dotenv::dotenv().ok();
|
||||
@@ -35,32 +49,129 @@ async fn main() -> anyhow::Result<()> {
|
||||
|
||||
let state = SharedState(Arc::new(State::new().await?));
|
||||
|
||||
let app = Router::new()
|
||||
.route("/", get(root))
|
||||
.with_state(state.clone())
|
||||
.layer(
|
||||
TraceLayer::new_for_http().make_span_with(|request: &Request<_>| {
|
||||
// Log the matched route's path (with placeholders not filled in).
|
||||
// Use request.uri() or OriginalUri if you want the real path.
|
||||
let matched_path = request
|
||||
.extensions()
|
||||
.get::<MatchedPath>()
|
||||
.map(MatchedPath::as_str);
|
||||
notmad::Mad::builder()
|
||||
.add_fn({
|
||||
let state = state.clone();
|
||||
move |_cancel| {
|
||||
let state = state.clone();
|
||||
|
||||
tracing::info_span!(
|
||||
"http_request",
|
||||
method = ?request.method(),
|
||||
matched_path,
|
||||
some_other_field = tracing::field::Empty,
|
||||
)
|
||||
}), // ...
|
||||
);
|
||||
async move {
|
||||
let app = Router::new()
|
||||
.route("/", get(root))
|
||||
.route("/metrics", get(metrics))
|
||||
.with_state(state.clone())
|
||||
.layer(TraceLayer::new_for_http().make_span_with(
|
||||
|request: &Request<_>| {
|
||||
// Log the matched route's path (with placeholders not filled in).
|
||||
// Use request.uri() or OriginalUri if you want the real path.
|
||||
let matched_path = request
|
||||
.extensions()
|
||||
.get::<MatchedPath>()
|
||||
.map(MatchedPath::as_str);
|
||||
|
||||
tracing::info!("listening on {}", host);
|
||||
let listener = tokio::net::TcpListener::bind(host).await.unwrap();
|
||||
axum::serve(listener, app.into_make_service())
|
||||
.await
|
||||
.unwrap();
|
||||
tracing::info_span!(
|
||||
"http_request",
|
||||
method = ?request.method(),
|
||||
matched_path,
|
||||
some_other_field = tracing::field::Empty,
|
||||
)
|
||||
},
|
||||
))
|
||||
.layer(
|
||||
CorsLayer::new()
|
||||
.allow_origin(Any)
|
||||
.allow_methods([Method::GET]),
|
||||
);
|
||||
|
||||
tracing::info!("listening on {}", host);
|
||||
|
||||
let listener = tokio::net::TcpListener::bind(host).await.unwrap();
|
||||
axum::serve(listener, app.into_make_service())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
})
|
||||
.add_fn({
|
||||
let state = state.clone();
|
||||
|
||||
move |cancel| {
|
||||
let state = state.clone();
|
||||
|
||||
async move {
|
||||
let nodrift_cancel =
|
||||
nodrift::schedule(std::time::Duration::from_secs(10), {
|
||||
let state = state.clone();
|
||||
|
||||
move || {
|
||||
let state = state.clone();
|
||||
|
||||
async move {
|
||||
state
|
||||
.event_metrics
|
||||
.prune_old(std::time::Duration::from_secs(60 * 2))
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
tokio::select! {
|
||||
_ = cancel.cancelled() => {
|
||||
nodrift_cancel.cancel();
|
||||
}
|
||||
_ = nodrift_cancel.cancelled() => {
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
})
|
||||
.add_fn(move |cancel| {
|
||||
let state = state.clone();
|
||||
async move {
|
||||
let nodrift_cancel = nodrift::schedule(std::time::Duration::from_millis(1), {
|
||||
let state = state.clone();
|
||||
move || {
|
||||
let state = state.clone();
|
||||
let mut rng = rand::thread_rng();
|
||||
let category_index = rng.gen_range(0..CATEGORIES.len());
|
||||
|
||||
async move {
|
||||
state
|
||||
.event_metrics
|
||||
.push_event(Event {
|
||||
event_name: CATEGORIES[category_index].to_string(),
|
||||
timestamp: std::time::SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs()
|
||||
as usize,
|
||||
})
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
tokio::select! {
|
||||
_ = cancel.cancelled() => {
|
||||
nodrift_cancel.cancel();
|
||||
}
|
||||
_ = nodrift_cancel.cancelled() => {
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
})
|
||||
.run()
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -72,6 +183,116 @@ async fn root() -> Html<String> {
|
||||
Html(INDEX.to_string())
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct MetricsQuery {
|
||||
start: usize,
|
||||
end: usize,
|
||||
}
|
||||
|
||||
async fn metrics(
|
||||
axum::extract::State(state): axum::extract::State<SharedState>,
|
||||
axum::extract::Query(query): axum::extract::Query<MetricsQuery>,
|
||||
) -> Json<Metrics> {
|
||||
let metrics = state
|
||||
.event_metrics
|
||||
.get_event_metrics(query.start, query.end)
|
||||
.await
|
||||
.expect("to be able to get event metrics");
|
||||
|
||||
Json(metrics)
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Event {
|
||||
pub event_name: String,
|
||||
pub timestamp: usize,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, Clone)]
|
||||
pub struct EventMetric {
|
||||
pub event_name: String,
|
||||
pub amount: usize,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, Clone)]
|
||||
pub struct Metrics {
|
||||
pub metrics: Vec<EventMetric>,
|
||||
pub since: usize,
|
||||
pub end: usize,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub struct EventMetrics {
|
||||
queue: Arc<RwLock<Vec<Event>>>,
|
||||
}
|
||||
|
||||
impl EventMetrics {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
queue: Arc::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn push_event(&self, event: Event) {
|
||||
let mut queue = self.queue.write().await;
|
||||
queue.push(event);
|
||||
}
|
||||
|
||||
pub async fn prune_old(&self, cutoff: std::time::Duration) {
|
||||
let cutoff_time = std::time::SystemTime::now()
|
||||
.checked_sub(cutoff)
|
||||
.unwrap()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs() as usize;
|
||||
|
||||
tracing::info!(before = cutoff_time, "pruning old events");
|
||||
|
||||
let mut queue = self.queue.write().await;
|
||||
let new_queue: Vec<_> = queue
|
||||
.iter()
|
||||
.filter(|&i| i.timestamp >= cutoff_time)
|
||||
.cloned()
|
||||
.collect();
|
||||
|
||||
tracing::info!(pruned = queue.len() - new_queue.len(), "pruned events");
|
||||
*queue = new_queue
|
||||
}
|
||||
|
||||
pub async fn get_event_metrics(&self, since: usize, end: usize) -> anyhow::Result<Metrics> {
|
||||
let queue = self.queue.read().await;
|
||||
|
||||
let items = queue
|
||||
.iter()
|
||||
.filter(|i| i.timestamp >= since && i.timestamp < end)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let mut metrics = BTreeMap::<String, EventMetric>::new();
|
||||
for item in items {
|
||||
match metrics.get_mut(&item.event_name) {
|
||||
Some(metrics) => {
|
||||
metrics.amount += 1;
|
||||
}
|
||||
None => {
|
||||
metrics.insert(
|
||||
item.event_name.clone(),
|
||||
EventMetric {
|
||||
event_name: item.event_name.clone(),
|
||||
amount: 1,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Metrics {
|
||||
metrics: metrics.values().cloned().collect(),
|
||||
since,
|
||||
end,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct SharedState(Arc<State>);
|
||||
|
||||
@@ -83,10 +304,14 @@ impl Deref for SharedState {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct State {}
|
||||
pub struct State {
|
||||
event_metrics: EventMetrics,
|
||||
}
|
||||
|
||||
impl State {
|
||||
pub async fn new() -> anyhow::Result<Self> {
|
||||
Ok(Self {})
|
||||
Ok(Self {
|
||||
event_metrics: EventMetrics::new(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user