use serde::Serialize;
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
#[derive(Clone, Debug, Serialize)]
pub struct LogLine {
pub level: String,
pub target: String,
pub message: String,
pub request_id: Option<String>,
pub at_ms: u64,
}
#[derive(Clone, Debug, Serialize)]
pub struct QueryLine {
pub sql: String,
pub duration_ms: Option<f64>,
pub rows: Option<u64>,
}
#[derive(Clone, Debug, Serialize)]
pub struct HttpLine {
pub method: String,
pub url: String,
pub status: Option<u16>,
pub duration_ms: Option<f64>,
pub error: Option<String>,
}
#[derive(Clone, Debug, Serialize)]
pub struct MailLine {
pub to: Vec<String>,
pub subject: String,
pub backend: String,
}
#[derive(Clone, Debug, Serialize)]
pub struct JobLine {
pub name: String,
pub status: String,
pub detail: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub duration_ms: Option<f64>,
}
#[derive(Clone, Debug, Serialize)]
pub struct CacheLine {
pub op: String,
pub key: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub hit: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub bytes: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub duration_ms: Option<f64>,
pub backend: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub ok: Option<bool>,
}
#[derive(Clone, Debug, Default, Serialize)]
pub struct RouteSnap {
pub path: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub pattern: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub captures: Vec<(String, String)>,
}
#[derive(Clone, Debug, Default, Serialize)]
pub struct RateLimitSnap {
#[serde(skip_serializing_if = "Option::is_none")]
pub limit: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub remaining: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub reset: Option<u64>,
}
#[derive(Clone, Debug, Default, Serialize)]
pub struct AuthSnap {
pub session_id: Option<String>,
pub user_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub email: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub roles: Vec<String>,
pub session_keys: Vec<(String, String)>,
}
#[derive(Clone, Debug, Serialize)]
pub struct RequestSnapshot {
pub id: String,
pub request_id: String,
pub method: String,
pub path: String,
pub status: u16,
pub duration_ms: f64,
pub at_ms: u64,
pub logs: Vec<LogLine>,
pub queries: Vec<QueryLine>,
pub http: Vec<HttpLine>,
pub mail: Vec<MailLine>,
pub jobs: Vec<JobLine>,
pub cache: Vec<CacheLine>,
pub auth: AuthSnap,
pub route: RouteSnap,
#[serde(skip_serializing_if = "Option::is_none")]
pub locale: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub csrf: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub rate_limit: Option<RateLimitSnap>,
#[serde(skip_serializing_if = "Option::is_none")]
pub encoding: Option<String>,
}
#[derive(Clone, Debug, Serialize)]
pub struct RequestMeta {
pub id: String,
pub request_id: String,
pub method: String,
pub path: String,
pub status: u16,
pub duration_ms: f64,
pub at_ms: u64,
pub sql_count: usize,
pub log_errors: usize,
pub http_count: usize,
pub mail_count: usize,
pub cache_count: usize,
pub job_count: usize,
}
impl From<&RequestSnapshot> for RequestMeta {
fn from(s: &RequestSnapshot) -> Self {
Self {
id: s.id.clone(),
request_id: s.request_id.clone(),
method: s.method.clone(),
path: s.path.clone(),
status: s.status,
duration_ms: s.duration_ms,
at_ms: s.at_ms,
sql_count: s.queries.len(),
log_errors: s
.logs
.iter()
.filter(|l| l.level.eq_ignore_ascii_case("ERROR") || l.level == "ERROR")
.count(),
http_count: s.http.len(),
mail_count: s.mail.len(),
cache_count: s.cache.len(),
job_count: s.jobs.len(),
}
}
}
#[derive(Default)]
struct BagInner {
logs: Vec<LogLine>,
queries: Vec<QueryLine>,
http: Vec<HttpLine>,
mail: Vec<MailLine>,
jobs: Vec<JobLine>,
cache: Vec<CacheLine>,
auth: AuthSnap,
route: RouteSnap,
locale: Option<String>,
csrf: Option<bool>,
rate_limit: Option<RateLimitSnap>,
encoding: Option<String>,
}
#[derive(Clone)]
pub struct DevToolsBag {
pub id: String,
pub request_id: String,
pub method: String,
pub path: String,
pub started: Instant,
inner: Arc<Mutex<BagInner>>,
}
impl DevToolsBag {
pub fn new(id: String, request_id: String, method: String, path: String) -> Self {
Self {
id,
request_id,
method,
path: path.clone(),
started: Instant::now(),
inner: Arc::new(Mutex::new(BagInner {
route: RouteSnap {
path,
..Default::default()
},
..Default::default()
})),
}
}
pub fn push_log(&self, line: LogLine) {
let mut g = self.inner.lock().unwrap();
if g.logs.len() < 200 {
g.logs.push(line);
}
}
pub fn push_query(&self, q: QueryLine) {
let mut g = self.inner.lock().unwrap();
if g.queries.len() < 200 {
g.queries.push(q);
}
}
pub fn push_http(&self, h: HttpLine) {
let mut g = self.inner.lock().unwrap();
if g.http.len() < 100 {
g.http.push(h);
}
}
pub fn push_mail(&self, m: MailLine) {
let mut g = self.inner.lock().unwrap();
if g.mail.len() < 50 {
g.mail.push(m);
}
}
pub fn push_job(&self, j: JobLine) {
let mut g = self.inner.lock().unwrap();
if g.jobs.len() < 50 {
g.jobs.push(j);
}
}
pub fn push_cache(&self, c: CacheLine) {
let mut g = self.inner.lock().unwrap();
if g.cache.len() < 200 {
g.cache.push(c);
}
}
pub fn set_auth(&self, auth: AuthSnap) {
self.inner.lock().unwrap().auth = auth;
}
pub fn set_route(&self, route: RouteSnap) {
self.inner.lock().unwrap().route = route;
}
pub fn set_locale(&self, locale: Option<String>) {
self.inner.lock().unwrap().locale = locale;
}
pub fn set_csrf(&self, present: Option<bool>) {
self.inner.lock().unwrap().csrf = present;
}
pub fn set_rate_limit(&self, rl: Option<RateLimitSnap>) {
self.inner.lock().unwrap().rate_limit = rl;
}
pub fn set_encoding(&self, encoding: Option<String>) {
self.inner.lock().unwrap().encoding = encoding;
}
pub fn finish(self, status: u16) -> RequestSnapshot {
let duration_ms = self.started.elapsed().as_secs_f64() * 1000.0;
let at_ms = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or(Duration::ZERO)
.as_millis() as u64;
let inner = self.inner.lock().unwrap();
RequestSnapshot {
id: self.id,
request_id: self.request_id,
method: self.method,
path: self.path,
status,
duration_ms,
at_ms,
logs: inner.logs.clone(),
queries: inner.queries.clone(),
http: inner.http.clone(),
mail: inner.mail.clone(),
jobs: inner.jobs.clone(),
cache: inner.cache.clone(),
auth: inner.auth.clone(),
route: inner.route.clone(),
locale: inner.locale.clone(),
csrf: inner.csrf,
rate_limit: inner.rate_limit.clone(),
encoding: inner.encoding.clone(),
}
}
}
pub fn now_ms() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or(Duration::ZERO)
.as_millis() as u64
}
pub fn truncate_key(key: &str, max: usize) -> String {
if key.len() <= max {
key.to_string()
} else {
format!("{}…", &key[..max.saturating_sub(1)])
}
}