cargo fmt: format all Rust source files
All checks were successful
CI — P1 Route (Rust) / test (push) Successful in 6m35s
All checks were successful
CI — P1 Route (Rust) / test (push) Successful in 6m35s
This commit is contained in:
@@ -4,7 +4,7 @@ use uuid::Uuid;
|
||||
|
||||
async fn get_org_owner_email(pool: &PgPool, org_id: Uuid) -> Result<String, anyhow::Error> {
|
||||
let row = sqlx::query_as::<_, (String,)>(
|
||||
"SELECT email FROM users WHERE org_id = $1 AND role = 'owner' LIMIT 1"
|
||||
"SELECT email FROM users WHERE org_id = $1 AND role = 'owner' LIMIT 1",
|
||||
)
|
||||
.bind(org_id)
|
||||
.fetch_one(pool)
|
||||
@@ -18,7 +18,7 @@ async fn get_org_owner_email(pool: &PgPool, org_id: Uuid) -> Result<String, anyh
|
||||
pub async fn check_anomalies(ts_pool: &PgPool, pg_pool: &PgPool) -> anyhow::Result<()> {
|
||||
// Get orgs with recent activity
|
||||
let orgs = sqlx::query_as::<_, (Uuid,)>(
|
||||
"SELECT DISTINCT org_id FROM request_events WHERE time >= now() - interval '1 hour'"
|
||||
"SELECT DISTINCT org_id FROM request_events WHERE time >= now() - interval '1 hour'",
|
||||
)
|
||||
.fetch_all(ts_pool)
|
||||
.await?;
|
||||
@@ -35,7 +35,7 @@ async fn check_org_anomaly(ts_pool: &PgPool, pg_pool: &PgPool, org_id: Uuid) ->
|
||||
let current = sqlx::query_as::<_, (f64,)>(
|
||||
"SELECT COALESCE(SUM(cost_actual), 0)::float8
|
||||
FROM request_events
|
||||
WHERE org_id = $1 AND time >= now() - interval '1 hour'"
|
||||
WHERE org_id = $1 AND time >= now() - interval '1 hour'",
|
||||
)
|
||||
.bind(org_id)
|
||||
.fetch_one(ts_pool)
|
||||
@@ -88,11 +88,7 @@ async fn check_org_anomaly(ts_pool: &PgPool, pg_pool: &PgPool, org_id: Uuid) ->
|
||||
]
|
||||
});
|
||||
let client = reqwest::Client::new();
|
||||
if let Err(e) = client.post(&slack_url)
|
||||
.json(&payload)
|
||||
.send()
|
||||
.await
|
||||
{
|
||||
if let Err(e) = client.post(&slack_url).json(&payload).send().await {
|
||||
warn!(error = %e, "Failed to send Slack anomaly alert");
|
||||
}
|
||||
}
|
||||
@@ -114,7 +110,8 @@ async fn check_org_anomaly(ts_pool: &PgPool, pg_pool: &PgPool, org_id: Uuid) ->
|
||||
)
|
||||
});
|
||||
let client = reqwest::Client::new();
|
||||
if let Err(e) = client.post("https://api.resend.com/emails")
|
||||
if let Err(e) = client
|
||||
.post("https://api.resend.com/emails")
|
||||
.bearer_auth(&resend_key)
|
||||
.json(&email_body)
|
||||
.send()
|
||||
|
||||
@@ -6,19 +6,17 @@ use uuid::Uuid;
|
||||
/// Calculate next Monday 9 AM UTC from a given time
|
||||
pub fn next_monday_9am(from: DateTime<Utc>) -> DateTime<Utc> {
|
||||
let days_until_monday = (7 - from.weekday().num_days_from_monday()) % 7;
|
||||
let days_until_monday = if days_until_monday == 0 && from.time() >= NaiveTime::from_hms_opt(9, 0, 0).unwrap() {
|
||||
7 // Already past Monday 9 AM, go to next week
|
||||
} else if days_until_monday == 0 {
|
||||
0 // It's Monday but before 9 AM
|
||||
} else {
|
||||
days_until_monday
|
||||
};
|
||||
let days_until_monday =
|
||||
if days_until_monday == 0 && from.time() >= NaiveTime::from_hms_opt(9, 0, 0).unwrap() {
|
||||
7 // Already past Monday 9 AM, go to next week
|
||||
} else if days_until_monday == 0 {
|
||||
0 // It's Monday but before 9 AM
|
||||
} else {
|
||||
days_until_monday
|
||||
};
|
||||
|
||||
let target_date = from.date_naive() + chrono::Duration::days(days_until_monday as i64);
|
||||
target_date
|
||||
.and_hms_opt(9, 0, 0)
|
||||
.unwrap()
|
||||
.and_utc()
|
||||
target_date.and_hms_opt(9, 0, 0).unwrap().and_utc()
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -58,7 +56,7 @@ pub async fn generate_all_digests(ts_pool: &PgPool, pg_pool: &PgPool) -> anyhow:
|
||||
WHERE o.id IN (
|
||||
SELECT DISTINCT org_id FROM request_events
|
||||
WHERE time >= now() - interval '7 days'
|
||||
)"
|
||||
)",
|
||||
)
|
||||
.fetch_all(pg_pool)
|
||||
.await?;
|
||||
@@ -70,13 +68,27 @@ pub async fn generate_all_digests(ts_pool: &PgPool, pg_pool: &PgPool) -> anyhow:
|
||||
Ok(digest) => {
|
||||
// Send weekly digest via Resend
|
||||
if let Ok(resend_key) = std::env::var("RESEND_API_KEY") {
|
||||
let models_html: String = digest.top_models.iter().map(|m| {
|
||||
format!("<tr><td>{}</td><td>{}</td><td>${:.4}</td></tr>", m.model, m.request_count, m.cost)
|
||||
}).collect();
|
||||
let models_html: String = digest
|
||||
.top_models
|
||||
.iter()
|
||||
.map(|m| {
|
||||
format!(
|
||||
"<tr><td>{}</td><td>{}</td><td>${:.4}</td></tr>",
|
||||
m.model, m.request_count, m.cost
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
let savings_html: String = digest.top_savings.iter().map(|s| {
|
||||
format!("<tr><td>{} → {}</td><td>{}</td><td>${:.4}</td></tr>", s.original_model, s.routed_model, s.requests_routed, s.cost_saved)
|
||||
}).collect();
|
||||
let savings_html: String = digest
|
||||
.top_savings
|
||||
.iter()
|
||||
.map(|s| {
|
||||
format!(
|
||||
"<tr><td>{} → {}</td><td>{}</td><td>${:.4}</td></tr>",
|
||||
s.original_model, s.routed_model, s.requests_routed, s.cost_saved
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
let html = format!(
|
||||
"<h2>Weekly Cost Digest: {}</h2>\
|
||||
@@ -93,10 +105,14 @@ pub async fn generate_all_digests(ts_pool: &PgPool, pg_pool: &PgPool) -> anyhow:
|
||||
<table style='border-collapse:collapse;width:100%'>\
|
||||
<tr><th>Route</th><th>Requests</th><th>Saved</th></tr>{}</table>\
|
||||
<p><a href='https://route.dd0c.dev/dashboard'>View Dashboard →</a></p>",
|
||||
digest.org_name, digest.total_requests,
|
||||
digest.total_cost_original, digest.total_cost_actual,
|
||||
digest.total_cost_saved, digest.savings_pct,
|
||||
models_html, savings_html
|
||||
digest.org_name,
|
||||
digest.total_requests,
|
||||
digest.total_cost_original,
|
||||
digest.total_cost_actual,
|
||||
digest.total_cost_saved,
|
||||
digest.savings_pct,
|
||||
models_html,
|
||||
savings_html
|
||||
);
|
||||
|
||||
let email_body = serde_json::json!({
|
||||
@@ -107,7 +123,8 @@ pub async fn generate_all_digests(ts_pool: &PgPool, pg_pool: &PgPool) -> anyhow:
|
||||
});
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
match client.post("https://api.resend.com/emails")
|
||||
match client
|
||||
.post("https://api.resend.com/emails")
|
||||
.bearer_auth(&resend_key)
|
||||
.json(&email_body)
|
||||
.send()
|
||||
@@ -134,7 +151,11 @@ pub async fn generate_all_digests(ts_pool: &PgPool, pg_pool: &PgPool) -> anyhow:
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn generate_digest(ts_pool: &PgPool, org_id: Uuid, org_name: &str) -> anyhow::Result<DigestData> {
|
||||
async fn generate_digest(
|
||||
ts_pool: &PgPool,
|
||||
org_id: Uuid,
|
||||
org_name: &str,
|
||||
) -> anyhow::Result<DigestData> {
|
||||
// Summary stats
|
||||
let summary = sqlx::query_as::<_, (i64, f64, f64, f64)>(
|
||||
"SELECT COUNT(*),
|
||||
@@ -142,7 +163,7 @@ async fn generate_digest(ts_pool: &PgPool, org_id: Uuid, org_name: &str) -> anyh
|
||||
COALESCE(SUM(cost_actual), 0)::float8,
|
||||
COALESCE(SUM(cost_saved), 0)::float8
|
||||
FROM request_events
|
||||
WHERE org_id = $1 AND time >= now() - interval '7 days'"
|
||||
WHERE org_id = $1 AND time >= now() - interval '7 days'",
|
||||
)
|
||||
.bind(org_id)
|
||||
.fetch_one(ts_pool)
|
||||
@@ -155,7 +176,7 @@ async fn generate_digest(ts_pool: &PgPool, org_id: Uuid, org_name: &str) -> anyh
|
||||
WHERE org_id = $1 AND time >= now() - interval '7 days'
|
||||
GROUP BY original_model
|
||||
ORDER BY SUM(cost_actual) DESC
|
||||
LIMIT 5"
|
||||
LIMIT 5",
|
||||
)
|
||||
.bind(org_id)
|
||||
.fetch_all(ts_pool)
|
||||
@@ -168,13 +189,17 @@ async fn generate_digest(ts_pool: &PgPool, org_id: Uuid, org_name: &str) -> anyh
|
||||
WHERE org_id = $1 AND time >= now() - interval '7 days' AND strategy != 'passthrough'
|
||||
GROUP BY original_model, routed_model
|
||||
ORDER BY SUM(cost_saved) DESC
|
||||
LIMIT 5"
|
||||
LIMIT 5",
|
||||
)
|
||||
.bind(org_id)
|
||||
.fetch_all(ts_pool)
|
||||
.await?;
|
||||
|
||||
let savings_pct = if summary.1 > 0.0 { (summary.3 / summary.1) * 100.0 } else { 0.0 };
|
||||
let savings_pct = if summary.1 > 0.0 {
|
||||
(summary.3 / summary.1) * 100.0
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
|
||||
Ok(DigestData {
|
||||
_org_id: org_id,
|
||||
@@ -184,17 +209,23 @@ async fn generate_digest(ts_pool: &PgPool, org_id: Uuid, org_name: &str) -> anyh
|
||||
total_cost_actual: summary.2,
|
||||
total_cost_saved: summary.3,
|
||||
savings_pct,
|
||||
top_models: top_models.iter().map(|r| ModelUsage {
|
||||
model: r.0.clone(),
|
||||
request_count: r.1,
|
||||
cost: r.2,
|
||||
}).collect(),
|
||||
top_savings: top_savings.iter().map(|r| RoutingSaving {
|
||||
original_model: r.0.clone(),
|
||||
routed_model: r.1.clone(),
|
||||
requests_routed: r.2,
|
||||
cost_saved: r.3,
|
||||
}).collect(),
|
||||
top_models: top_models
|
||||
.iter()
|
||||
.map(|r| ModelUsage {
|
||||
model: r.0.clone(),
|
||||
request_count: r.1,
|
||||
cost: r.2,
|
||||
})
|
||||
.collect(),
|
||||
top_savings: top_savings
|
||||
.iter()
|
||||
.map(|r| RoutingSaving {
|
||||
original_model: r.0.clone(),
|
||||
routed_model: r.1.clone(),
|
||||
requests_routed: r.2,
|
||||
cost_saved: r.3,
|
||||
})
|
||||
.collect(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -205,27 +236,45 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn next_monday_from_wednesday() {
|
||||
let wed = chrono::NaiveDate::from_ymd_opt(2026, 3, 4).unwrap() // Wednesday
|
||||
.and_hms_opt(14, 0, 0).unwrap().and_utc();
|
||||
let wed = chrono::NaiveDate::from_ymd_opt(2026, 3, 4)
|
||||
.unwrap() // Wednesday
|
||||
.and_hms_opt(14, 0, 0)
|
||||
.unwrap()
|
||||
.and_utc();
|
||||
let next = next_monday_9am(wed);
|
||||
assert_eq!(next.weekday(), Weekday::Mon);
|
||||
assert_eq!(next.date_naive(), chrono::NaiveDate::from_ymd_opt(2026, 3, 9).unwrap());
|
||||
assert_eq!(
|
||||
next.date_naive(),
|
||||
chrono::NaiveDate::from_ymd_opt(2026, 3, 9).unwrap()
|
||||
);
|
||||
assert_eq!(next.time(), NaiveTime::from_hms_opt(9, 0, 0).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn next_monday_from_monday_before_9am() {
|
||||
let mon = chrono::NaiveDate::from_ymd_opt(2026, 3, 2).unwrap() // Monday
|
||||
.and_hms_opt(8, 0, 0).unwrap().and_utc();
|
||||
let mon = chrono::NaiveDate::from_ymd_opt(2026, 3, 2)
|
||||
.unwrap() // Monday
|
||||
.and_hms_opt(8, 0, 0)
|
||||
.unwrap()
|
||||
.and_utc();
|
||||
let next = next_monday_9am(mon);
|
||||
assert_eq!(next.date_naive(), chrono::NaiveDate::from_ymd_opt(2026, 3, 2).unwrap());
|
||||
assert_eq!(
|
||||
next.date_naive(),
|
||||
chrono::NaiveDate::from_ymd_opt(2026, 3, 2).unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn next_monday_from_monday_after_9am() {
|
||||
let mon = chrono::NaiveDate::from_ymd_opt(2026, 3, 2).unwrap()
|
||||
.and_hms_opt(10, 0, 0).unwrap().and_utc();
|
||||
let mon = chrono::NaiveDate::from_ymd_opt(2026, 3, 2)
|
||||
.unwrap()
|
||||
.and_hms_opt(10, 0, 0)
|
||||
.unwrap()
|
||||
.and_utc();
|
||||
let next = next_monday_9am(mon);
|
||||
assert_eq!(next.date_naive(), chrono::NaiveDate::from_ymd_opt(2026, 3, 9).unwrap());
|
||||
assert_eq!(
|
||||
next.date_naive(),
|
||||
chrono::NaiveDate::from_ymd_opt(2026, 3, 9).unwrap()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
use std::sync::Arc;
|
||||
use tracing::{info, error};
|
||||
use chrono::Utc;
|
||||
use std::sync::Arc;
|
||||
use tracing::{error, info};
|
||||
|
||||
use dd0c_route::AppConfig;
|
||||
|
||||
mod digest;
|
||||
mod anomaly;
|
||||
mod digest;
|
||||
|
||||
/// Refresh model pricing from known provider pricing pages.
|
||||
/// Falls back to hardcoded defaults if fetch fails.
|
||||
@@ -83,7 +83,9 @@ async fn main() -> anyhow::Result<()> {
|
||||
loop {
|
||||
let now = Utc::now();
|
||||
let next_monday = digest::next_monday_9am(now);
|
||||
let sleep_duration = (next_monday - now).to_std().unwrap_or(std::time::Duration::from_secs(3600));
|
||||
let sleep_duration = (next_monday - now)
|
||||
.to_std()
|
||||
.unwrap_or(std::time::Duration::from_secs(3600));
|
||||
tokio::time::sleep(sleep_duration).await;
|
||||
|
||||
info!("Generating weekly digests");
|
||||
|
||||
Reference in New Issue
Block a user