From fe4cb4ffbbeb462f85600bc4eb8f4d40bdf48948 Mon Sep 17 00:00:00 2001 From: Jon Seager Date: Thu, 5 Feb 2026 18:14:00 +0000 Subject: [PATCH] feat(logging): add CRUD operation and timeline error logging - Add info! logging for entity create/update/delete across all route handlers - Add entity deleted logging in define_delete_handler! macro - Log timeline insert errors instead of silently discarding them --- src/application/routes/bags.rs | 13 +++++++++++-- src/application/routes/brews.rs | 10 ++++++++-- src/application/routes/cafes.rs | 4 ++++ src/application/routes/checkin.rs | 3 +++ src/application/routes/cups.rs | 3 +++ src/application/routes/gear.rs | 9 ++++++++- src/application/routes/macros.rs | 2 ++ src/application/routes/roasters.rs | 4 ++++ src/application/routes/roasts.rs | 5 +++++ src/application/routes/scan.rs | 7 ++++++- 10 files changed, 54 insertions(+), 6 deletions(-) diff --git a/src/application/routes/bags.rs b/src/application/routes/bags.rs index 275b437..620e56b 100644 --- a/src/application/routes/bags.rs +++ b/src/application/routes/bags.rs @@ -3,6 +3,7 @@ use axum::extract::{Path, Query, State}; use axum::http::{HeaderMap, StatusCode}; use axum::response::{IntoResponse, Redirect, Response}; use serde::Deserialize; +use tracing::{info, warn}; use super::macros::{define_delete_handler, define_enriched_get_handler}; use crate::application::auth::AuthenticatedUser; @@ -80,6 +81,8 @@ pub(crate) async fn create_bag( .await .map_err(AppError::from)?; + info!(bag_id = %bag.id, roast = %roast.name, "bag created"); + // Add timeline event let event = NewTimelineEvent { entity_type: "bag".to_string(), @@ -102,7 +105,9 @@ pub(crate) async fn create_bag( roaster_slug: Some(roaster.slug.clone()), brew_data: None, }; - let _ = state.timeline_repo.insert(event).await; + if let Err(err) = state.timeline_repo.insert(event).await { + warn!(error = %err, entity_type = "bag", "failed to record timeline event"); + } if is_datastar_request(&headers) { render_bag_list_fragment(state, request, search, true) @@ -181,6 +186,8 @@ pub(crate) async fn update_bag( .await .map_err(AppError::from)?; + info!(%id, closed = ?update.closed, "bag updated"); + if let Some(true) = update.closed { // Fetch roast and roaster for timeline event if let Ok(roast) = state.roast_repo.get(bag.roast_id).await @@ -207,7 +214,9 @@ pub(crate) async fn update_bag( roaster_slug: Some(roaster.slug.clone()), brew_data: None, }; - let _ = state.timeline_repo.insert(event).await; + if let Err(err) = state.timeline_repo.insert(event).await { + warn!(error = %err, entity_type = "bag", "failed to record timeline event"); + } } } diff --git a/src/application/routes/brews.rs b/src/application/routes/brews.rs index c8098ca..4cb5d72 100644 --- a/src/application/routes/brews.rs +++ b/src/application/routes/brews.rs @@ -3,6 +3,7 @@ use axum::extract::{Query, State}; use axum::http::{HeaderMap, StatusCode}; use axum::response::{IntoResponse, Redirect, Response}; use serde::{Deserialize, Deserializer}; +use tracing::{info, warn}; use super::macros::{define_delete_handler, define_enriched_get_handler}; use crate::application::auth::AuthenticatedUser; @@ -207,6 +208,8 @@ pub(crate) async fn create_brew( .await .map_err(AppError::from)?; + info!(brew_id = %brew.id, "brew created"); + // Fetch enriched brew details for timeline event let enriched = state .brew_repo @@ -215,10 +218,13 @@ pub(crate) async fn create_brew( .map_err(AppError::from)?; // Add timeline event - let _ = state + if let Err(err) = state .timeline_repo .insert(brew_timeline_event(&enriched)) - .await; + .await + { + warn!(error = %err, entity_type = "brew", "failed to record timeline event"); + } if is_datastar_request(&headers) { // Check if request came from timeline - return a script that redirects diff --git a/src/application/routes/cafes.rs b/src/application/routes/cafes.rs index 81b5cfd..607ba13 100644 --- a/src/application/routes/cafes.rs +++ b/src/application/routes/cafes.rs @@ -17,6 +17,7 @@ use crate::domain::listing::{ListRequest, SortDirection}; use crate::infrastructure::foursquare; use crate::presentation::web::templates::{CafeListTemplate, NearbyCafesFragment}; use crate::presentation::web::views::{CafeView, ListNavigator, NearbyCafeView, Paginated}; +use tracing::info; const CAFE_PAGE_PATH: &str = "/data?type=cafes"; const CAFE_FRAGMENT_PATH: &str = "/data?type=cafes#cafe-list"; @@ -70,6 +71,8 @@ pub(crate) async fn create_cafe( .await .map_err(AppError::from)?; + info!(cafe_id = %cafe.id, name = %cafe.name, "cafe created"); + if is_datastar_request(&headers) { render_cafe_list_fragment(state, request, search, true) .await @@ -108,6 +111,7 @@ pub(crate) async fn update_cafe( .update(id, payload) .await .map_err(AppError::from)?; + info!(%id, "cafe updated"); Ok(Json(cafe)) } diff --git a/src/application/routes/checkin.rs b/src/application/routes/checkin.rs index b741fb7..e8006ce 100644 --- a/src/application/routes/checkin.rs +++ b/src/application/routes/checkin.rs @@ -15,6 +15,7 @@ use crate::domain::cafes::NewCafe; use crate::domain::cups::NewCup; use crate::domain::ids::{CafeId, RoastId}; use crate::presentation::web::templates::CheckInTemplate; +use tracing::info; #[tracing::instrument(skip(state, cookies))] pub(crate) async fn checkin_page( @@ -115,6 +116,8 @@ pub(crate) async fn submit_checkin( .await .map_err(AppError::from)?; + info!(cup_id = %cup.id, %cafe_id, "check-in recorded"); + if is_datastar_request(&headers) { crate::application::routes::support::render_signals_json(&[]).map_err(ApiError::from) } else if matches!(source, PayloadSource::Form) { diff --git a/src/application/routes/cups.rs b/src/application/routes/cups.rs index 9fd4b35..79bd362 100644 --- a/src/application/routes/cups.rs +++ b/src/application/routes/cups.rs @@ -17,6 +17,7 @@ use crate::domain::ids::CupId; use crate::domain::listing::{ListRequest, SortDirection}; use crate::presentation::web::templates::CupListTemplate; use crate::presentation::web::views::{CupView, ListNavigator, Paginated}; +use tracing::info; const CUP_PAGE_PATH: &str = "/data?type=cups"; const CUP_FRAGMENT_PATH: &str = "/data?type=cups#cup-list"; @@ -60,6 +61,8 @@ pub(crate) async fn create_cup( .await .map_err(AppError::from)?; + info!(cup_id = %cup.id, "cup created"); + if is_datastar_request(&headers) { render_cup_list_fragment(state, request, search, true) .await diff --git a/src/application/routes/gear.rs b/src/application/routes/gear.rs index d70f388..f1d836c 100644 --- a/src/application/routes/gear.rs +++ b/src/application/routes/gear.rs @@ -5,6 +5,7 @@ use axum::extract::{Path, Query, State}; use axum::http::{HeaderMap, StatusCode}; use axum::response::{IntoResponse, Redirect, Response}; use serde::Deserialize; +use tracing::{info, warn}; use super::macros::{define_delete_handler, define_get_handler, define_list_fragment_renderer}; use crate::application::auth::AuthenticatedUser; @@ -63,6 +64,8 @@ pub(crate) async fn create_gear( .await .map_err(AppError::from)?; + info!(gear_id = %gear.id, make = %gear.make, model = %gear.model, "gear created"); + // Add timeline event let event = NewTimelineEvent { entity_type: "gear".to_string(), @@ -89,7 +92,9 @@ pub(crate) async fn create_gear( roaster_slug: None, // Gear is not related to roasters brew_data: None, }; - let _ = state.timeline_repo.insert(event).await; + if let Err(err) = state.timeline_repo.insert(event).await { + warn!(error = %err, entity_type = "gear", "failed to record timeline event"); + } if is_datastar_request(&headers) { render_gear_list_fragment(state, request, search, true) @@ -145,6 +150,8 @@ pub(crate) async fn update_gear( .await .map_err(AppError::from)?; + info!(%id, "gear updated"); + if is_datastar_request(&headers) { render_gear_list_fragment(state, request, search, true) .await diff --git a/src/application/routes/macros.rs b/src/application/routes/macros.rs index 6044428..38b0f53 100644 --- a/src/application/routes/macros.rs +++ b/src/application/routes/macros.rs @@ -95,6 +95,8 @@ macro_rules! define_delete_handler { .await .map_err(crate::application::errors::AppError::from)?; + tracing::info!(%id, "entity deleted"); + if crate::application::routes::support::is_datastar_request(&headers) { $render_fragment(state, request, search, true) .await diff --git a/src/application/routes/roasters.rs b/src/application/routes/roasters.rs index 5b1d372..e12e7de 100644 --- a/src/application/routes/roasters.rs +++ b/src/application/routes/roasters.rs @@ -16,6 +16,7 @@ use crate::domain::roasters::{NewRoaster, Roaster, RoasterSortKey, UpdateRoaster use crate::infrastructure::ai::{self, ExtractionInput}; use crate::presentation::web::templates::RoasterListTemplate; use crate::presentation::web::views::{ListNavigator, Paginated, RoasterView}; +use tracing::info; const ROASTER_PAGE_PATH: &str = "/data?type=roasters"; const ROASTER_FRAGMENT_PATH: &str = "/data?type=roasters#roaster-list"; @@ -71,6 +72,8 @@ pub(crate) async fn create_roaster( .await .map_err(AppError::from)?; + info!(roaster_id = %roaster.id, name = %roaster.name, "roaster created"); + if is_datastar_request(&headers) { render_roaster_list_fragment(state, request, search, true) .await @@ -107,6 +110,7 @@ pub(crate) async fn update_roaster( .update(id, payload) .await .map_err(AppError::from)?; + info!(%id, "roaster updated"); Ok(Json(roaster)) } diff --git a/src/application/routes/roasts.rs b/src/application/routes/roasts.rs index 422a800..2e374ee 100644 --- a/src/application/routes/roasts.rs +++ b/src/application/routes/roasts.rs @@ -19,6 +19,7 @@ use crate::domain::roasts::{NewRoast, RoastSortKey, RoastWithRoaster, UpdateRoas use crate::infrastructure::ai::{self, ExtractionInput}; use crate::presentation::web::templates::{RoastListTemplate, RoastOptionsTemplate}; use crate::presentation::web::views::{ListNavigator, Paginated, RoastView}; +use tracing::info; const ROAST_PAGE_PATH: &str = "/data?type=roasts"; const ROAST_FRAGMENT_PATH: &str = "/data?type=roasts#roast-list"; @@ -69,6 +70,8 @@ pub(crate) async fn create_roast( .await .map_err(AppError::from)?; + info!(roast_id = %roast.id, name = %roast.name, "roast created"); + if is_datastar_request(&headers) { render_roast_list_fragment(state, request, search, true) .await @@ -169,6 +172,8 @@ pub(crate) async fn update_roast( .await .map_err(AppError::from)?; + info!(%id, "roast updated"); + let enriched = state .roast_repo .get_with_roaster(id) diff --git a/src/application/routes/scan.rs b/src/application/routes/scan.rs index f3b92d7..623abd2 100644 --- a/src/application/routes/scan.rs +++ b/src/application/routes/scan.rs @@ -3,6 +3,7 @@ use axum::extract::State; use axum::http::{HeaderMap, StatusCode}; use axum::response::{IntoResponse, Response}; use serde::{Deserialize, Serialize}; +use tracing::{info, warn}; use crate::application::auth::AuthenticatedUser; use crate::application::errors::{ApiError, AppError}; @@ -285,6 +286,8 @@ pub(crate) async fn submit_scan( .await .map_err(AppError::from)?; + info!(roaster_id = %roaster.id, roast_id = %roast.id, roast_name = %roast.name, "scan created roast"); + // Optionally create a bag let wants_bag = submission .open_bag @@ -324,7 +327,9 @@ pub(crate) async fn submit_scan( roaster_slug: Some(roaster.slug.clone()), brew_data: None, }; - let _ = state.timeline_repo.insert(event).await; + if let Err(err) = state.timeline_repo.insert(event).await { + warn!(error = %err, entity_type = "bag", "failed to record timeline event"); + } } let redirect = format!("/roasters/{}/roasts/{}", roaster.slug, roast.slug);