| 1 | |
| 2 | |
| 3 | |
| 4 | |
| 5 | use axum::{ |
| 6 | Json, Router, |
| 7 | extract::DefaultBodyLimit, |
| 8 | http::{HeaderMap, StatusCode}, |
| 9 | routing::post, |
| 10 | }; |
| 11 | use serde::Deserialize; |
| 12 | |
| 13 | use crate::{ |
| 14 | auth::{self, MaybeViewer}, |
| 15 | logging::FRONTEND_TARGET, |
| 16 | state::AppState, |
| 17 | }; |
| 18 | |
| 19 | pub fn router() -> Router<AppState> { |
| 20 | Router::new().route("/api/v1/client-logs", post(ingest).layer(DefaultBodyLimit::max(64 * 1024))) |
| 21 | } |
| 22 | |
| 23 | #[derive(Deserialize)] |
| 24 | struct Batch { |
| 25 | entries: Vec<Entry>, |
| 26 | } |
| 27 | |
| 28 | #[derive(Deserialize)] |
| 29 | struct Entry { |
| 30 | level: String, |
| 31 | message: String, |
| 32 | url: String, |
| 33 | at: Option<String>, |
| 34 | stack: Option<String>, |
| 35 | } |
| 36 | |
| 37 | async fn ingest(MaybeViewer(viewer): MaybeViewer, headers: HeaderMap, Json(batch): Json<Batch>) -> StatusCode { |
| 38 | let user = viewer.as_ref().map(|v| v.name.as_str()).unwrap_or("-"); |
| 39 | let agent = auth::user_agent(&headers).unwrap_or("-"); |
| 40 | for entry in batch.entries.into_iter().take(50) { |
| 41 | let message: String = entry.message.chars().take(2000).collect(); |
| 42 | let stack: String = entry.stack.unwrap_or_default().chars().take(4000).collect(); |
| 43 | let at = entry.at.unwrap_or_default(); |
| 44 | match entry.level.as_str() { |
| 45 | "error" => tracing::error!(target: FRONTEND_TARGET, user, url = %entry.url, at, agent, stack, "{message}"), |
| 46 | "warn" => tracing::warn!(target: FRONTEND_TARGET, user, url = %entry.url, at, agent, "{message}"), |
| 47 | _ => tracing::info!(target: FRONTEND_TARGET, user, url = %entry.url, at, agent, "{message}"), |
| 48 | } |
| 49 | } |
| 50 | StatusCode::NO_CONTENT |
| 51 | } |