add structured logging
This commit is contained in:
+54
-51
@@ -1,6 +1,6 @@
|
||||
use crate::db;
|
||||
use crate::types::{
|
||||
Config, LandingDto, MonthlyAggregate, PriceTrend, SpeciesDto, SummaryDto, TopSpecies,
|
||||
Config, LandingDto, LookupDto, MonthlyAggregate, PriceTrend, SpeciesDto, SummaryDto, TopSpecies,
|
||||
};
|
||||
use axum::{
|
||||
Json, Router,
|
||||
@@ -394,50 +394,51 @@ async fn get_summary(
|
||||
async fn export_parquet(State(state): State<AppState>) -> ApiResult<Response> {
|
||||
let conn = state.conn.clone();
|
||||
|
||||
let (file_path, file) =
|
||||
tokio::task::spawn_blocking(move || -> ApiResult<(String, std::fs::File)> {
|
||||
let conn = conn.blocking_lock();
|
||||
let file_path = tokio::task::spawn_blocking(move || -> ApiResult<String> {
|
||||
let conn = conn.blocking_lock();
|
||||
|
||||
// Use builder to ensure consistent naming for cleanup
|
||||
let tmp = tempfile::Builder::new()
|
||||
.prefix("hagfish-")
|
||||
.suffix(".parquet")
|
||||
.tempfile()
|
||||
.map_err(|e| ApiError {
|
||||
status: StatusCode::INTERNAL_SERVER_ERROR,
|
||||
message: format!("Failed to create temp file: {e}"),
|
||||
})?;
|
||||
|
||||
let (_kept_file, path) = tmp.keep().map_err(|e| ApiError {
|
||||
let tmp = tempfile::Builder::new()
|
||||
.prefix("hagfish-")
|
||||
.suffix(".parquet")
|
||||
.tempfile()
|
||||
.map_err(|e| ApiError {
|
||||
status: StatusCode::INTERNAL_SERVER_ERROR,
|
||||
message: format!("Failed to persist temp file: {e}"),
|
||||
message: format!("Failed to create temp file: {e}"),
|
||||
})?;
|
||||
|
||||
let path_str = path
|
||||
.to_str()
|
||||
.ok_or_else(|| ApiError {
|
||||
status: StatusCode::INTERNAL_SERVER_ERROR,
|
||||
message: "Temp path contains invalid UTF-8".to_string(),
|
||||
})?
|
||||
.to_string();
|
||||
let (_kept_file, path) = tmp.keep().map_err(|e| ApiError {
|
||||
status: StatusCode::INTERNAL_SERVER_ERROR,
|
||||
message: format!("Failed to persist temp file: {e}"),
|
||||
})?;
|
||||
|
||||
db::export_parquet(&conn, &path_str)?;
|
||||
|
||||
let file = std::fs::File::open(&path_str).map_err(|e| ApiError {
|
||||
let path_str = path
|
||||
.to_str()
|
||||
.ok_or_else(|| ApiError {
|
||||
status: StatusCode::INTERNAL_SERVER_ERROR,
|
||||
message: format!("Failed to open exported file: {e}"),
|
||||
})?;
|
||||
message: "Temp path contains invalid UTF-8".to_string(),
|
||||
})?
|
||||
.to_string();
|
||||
|
||||
Ok((path_str, file))
|
||||
})
|
||||
db::export_parquet(&conn, &path_str)?;
|
||||
|
||||
drop(conn);
|
||||
|
||||
Ok(path_str)
|
||||
})
|
||||
.await
|
||||
.map_err(|e| ApiError {
|
||||
status: StatusCode::INTERNAL_SERVER_ERROR,
|
||||
message: format!("Task join error: {e}"),
|
||||
})??;
|
||||
|
||||
let file = tokio::fs::File::open(&file_path)
|
||||
.await
|
||||
.map_err(|e| ApiError {
|
||||
status: StatusCode::INTERNAL_SERVER_ERROR,
|
||||
message: format!("Task join error: {e}"),
|
||||
})??;
|
||||
message: format!("Failed to open exported file: {e}"),
|
||||
})?;
|
||||
|
||||
let tokio_file = tokio::fs::File::from(file);
|
||||
let stream = tokio_util::io::ReaderStream::new(tokio_file);
|
||||
let stream = tokio_util::io::ReaderStream::new(file);
|
||||
let body = Body::from_stream(stream);
|
||||
|
||||
let cleanup_path = file_path.clone();
|
||||
@@ -448,7 +449,7 @@ async fn export_parquet(State(state): State<AppState>) -> ApiResult<Response> {
|
||||
}
|
||||
});
|
||||
|
||||
Ok(Response::builder()
|
||||
Response::builder()
|
||||
.status(StatusCode::OK)
|
||||
.header(header::CONTENT_TYPE, "application/octet-stream")
|
||||
.header(
|
||||
@@ -456,7 +457,10 @@ async fn export_parquet(State(state): State<AppState>) -> ApiResult<Response> {
|
||||
"attachment; filename=\"landings.parquet\"",
|
||||
)
|
||||
.body(body)
|
||||
.unwrap())
|
||||
.map_err(|e| ApiError {
|
||||
status: StatusCode::INTERNAL_SERVER_ERROR,
|
||||
message: format!("Failed to build response: {e}"),
|
||||
})
|
||||
}
|
||||
|
||||
async fn static_handler(uri: axum::http::Uri) -> Response {
|
||||
@@ -498,13 +502,13 @@ async fn static_handler(uri: axum::http::Uri) -> Response {
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_zones(State(state): State<AppState>) -> ApiResult<Json<Vec<SpeciesDto>>> {
|
||||
async fn get_zones(State(state): State<AppState>) -> ApiResult<Json<Vec<LookupDto>>> {
|
||||
let conn = state.conn.clone();
|
||||
let rows = tokio::task::spawn_blocking(move || -> db::Result<Vec<SpeciesDto>> {
|
||||
let rows = tokio::task::spawn_blocking(move || -> db::Result<Vec<LookupDto>> {
|
||||
let conn = conn.blocking_lock();
|
||||
let mut stmt = conn.prepare("SELECT code, label FROM zone ORDER BY code")?;
|
||||
let rows = stmt.query_map([], |row| {
|
||||
Ok(SpeciesDto {
|
||||
Ok(LookupDto {
|
||||
code: row.get(0)?,
|
||||
label: row.get(1)?,
|
||||
})
|
||||
@@ -520,13 +524,13 @@ async fn get_zones(State(state): State<AppState>) -> ApiResult<Json<Vec<SpeciesD
|
||||
Ok(Json(rows))
|
||||
}
|
||||
|
||||
async fn get_gear(State(state): State<AppState>) -> ApiResult<Json<Vec<SpeciesDto>>> {
|
||||
async fn get_gear(State(state): State<AppState>) -> ApiResult<Json<Vec<LookupDto>>> {
|
||||
let conn = state.conn.clone();
|
||||
let rows = tokio::task::spawn_blocking(move || -> db::Result<Vec<SpeciesDto>> {
|
||||
let rows = tokio::task::spawn_blocking(move || -> db::Result<Vec<LookupDto>> {
|
||||
let conn = conn.blocking_lock();
|
||||
let mut stmt = conn.prepare("SELECT code, label FROM gear ORDER BY code")?;
|
||||
let rows = stmt.query_map([], |row| {
|
||||
Ok(SpeciesDto {
|
||||
Ok(LookupDto {
|
||||
code: row.get(0)?,
|
||||
label: row.get(1)?,
|
||||
})
|
||||
@@ -962,7 +966,7 @@ mod tests {
|
||||
#[tokio::test]
|
||||
async fn test_concurrent_export_requests() {
|
||||
let state = test_state();
|
||||
let app = build_router(state.clone());
|
||||
let app = build_router(state);
|
||||
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||||
let addr = listener.local_addr().unwrap();
|
||||
|
||||
@@ -970,12 +974,11 @@ mod tests {
|
||||
axum::serve(listener, app).await.unwrap();
|
||||
});
|
||||
|
||||
let resp1 = reqwest::get(format!("http://{addr}/api/export.parquet"))
|
||||
.await
|
||||
.unwrap();
|
||||
let resp2 = reqwest::get(format!("http://{addr}/api/export.parquet"))
|
||||
.await
|
||||
.unwrap();
|
||||
let url = format!("http://{addr}/api/export.parquet");
|
||||
let (resp1, resp2) = tokio::join!(reqwest::get(&url), reqwest::get(&url));
|
||||
|
||||
let resp1 = resp1.unwrap();
|
||||
let resp2 = resp2.unwrap();
|
||||
|
||||
assert_eq!(resp1.status(), StatusCode::OK);
|
||||
assert_eq!(resp2.status(), StatusCode::OK);
|
||||
@@ -985,8 +988,8 @@ mod tests {
|
||||
|
||||
assert_eq!(&bytes1[..4], b"PAR1");
|
||||
assert_eq!(&bytes2[..4], b"PAR1");
|
||||
assert_ne!(bytes1.len(), 0);
|
||||
assert_ne!(bytes2.len(), 0);
|
||||
assert!(!bytes1.is_empty());
|
||||
assert!(!bytes2.is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
Reference in New Issue
Block a user