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
This commit is contained in:
Jon Seager 2026-02-05 18:14:00 +00:00
parent 4ab00dd630
commit fe4cb4ffbb
No known key found for this signature in database
10 changed files with 54 additions and 6 deletions

View file

@ -3,6 +3,7 @@ use axum::extract::{Path, Query, State};
use axum::http::{HeaderMap, StatusCode}; use axum::http::{HeaderMap, StatusCode};
use axum::response::{IntoResponse, Redirect, Response}; use axum::response::{IntoResponse, Redirect, Response};
use serde::Deserialize; use serde::Deserialize;
use tracing::{info, warn};
use super::macros::{define_delete_handler, define_enriched_get_handler}; use super::macros::{define_delete_handler, define_enriched_get_handler};
use crate::application::auth::AuthenticatedUser; use crate::application::auth::AuthenticatedUser;
@ -80,6 +81,8 @@ pub(crate) async fn create_bag(
.await .await
.map_err(AppError::from)?; .map_err(AppError::from)?;
info!(bag_id = %bag.id, roast = %roast.name, "bag created");
// Add timeline event // Add timeline event
let event = NewTimelineEvent { let event = NewTimelineEvent {
entity_type: "bag".to_string(), entity_type: "bag".to_string(),
@ -102,7 +105,9 @@ pub(crate) async fn create_bag(
roaster_slug: Some(roaster.slug.clone()), roaster_slug: Some(roaster.slug.clone()),
brew_data: None, 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) { if is_datastar_request(&headers) {
render_bag_list_fragment(state, request, search, true) render_bag_list_fragment(state, request, search, true)
@ -181,6 +186,8 @@ pub(crate) async fn update_bag(
.await .await
.map_err(AppError::from)?; .map_err(AppError::from)?;
info!(%id, closed = ?update.closed, "bag updated");
if let Some(true) = update.closed { if let Some(true) = update.closed {
// Fetch roast and roaster for timeline event // Fetch roast and roaster for timeline event
if let Ok(roast) = state.roast_repo.get(bag.roast_id).await 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()), roaster_slug: Some(roaster.slug.clone()),
brew_data: None, 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");
}
} }
} }

View file

@ -3,6 +3,7 @@ use axum::extract::{Query, State};
use axum::http::{HeaderMap, StatusCode}; use axum::http::{HeaderMap, StatusCode};
use axum::response::{IntoResponse, Redirect, Response}; use axum::response::{IntoResponse, Redirect, Response};
use serde::{Deserialize, Deserializer}; use serde::{Deserialize, Deserializer};
use tracing::{info, warn};
use super::macros::{define_delete_handler, define_enriched_get_handler}; use super::macros::{define_delete_handler, define_enriched_get_handler};
use crate::application::auth::AuthenticatedUser; use crate::application::auth::AuthenticatedUser;
@ -207,6 +208,8 @@ pub(crate) async fn create_brew(
.await .await
.map_err(AppError::from)?; .map_err(AppError::from)?;
info!(brew_id = %brew.id, "brew created");
// Fetch enriched brew details for timeline event // Fetch enriched brew details for timeline event
let enriched = state let enriched = state
.brew_repo .brew_repo
@ -215,10 +218,13 @@ pub(crate) async fn create_brew(
.map_err(AppError::from)?; .map_err(AppError::from)?;
// Add timeline event // Add timeline event
let _ = state if let Err(err) = state
.timeline_repo .timeline_repo
.insert(brew_timeline_event(&enriched)) .insert(brew_timeline_event(&enriched))
.await; .await
{
warn!(error = %err, entity_type = "brew", "failed to record timeline event");
}
if is_datastar_request(&headers) { if is_datastar_request(&headers) {
// Check if request came from timeline - return a script that redirects // Check if request came from timeline - return a script that redirects

View file

@ -17,6 +17,7 @@ use crate::domain::listing::{ListRequest, SortDirection};
use crate::infrastructure::foursquare; use crate::infrastructure::foursquare;
use crate::presentation::web::templates::{CafeListTemplate, NearbyCafesFragment}; use crate::presentation::web::templates::{CafeListTemplate, NearbyCafesFragment};
use crate::presentation::web::views::{CafeView, ListNavigator, NearbyCafeView, Paginated}; use crate::presentation::web::views::{CafeView, ListNavigator, NearbyCafeView, Paginated};
use tracing::info;
const CAFE_PAGE_PATH: &str = "/data?type=cafes"; const CAFE_PAGE_PATH: &str = "/data?type=cafes";
const CAFE_FRAGMENT_PATH: &str = "/data?type=cafes#cafe-list"; const CAFE_FRAGMENT_PATH: &str = "/data?type=cafes#cafe-list";
@ -70,6 +71,8 @@ pub(crate) async fn create_cafe(
.await .await
.map_err(AppError::from)?; .map_err(AppError::from)?;
info!(cafe_id = %cafe.id, name = %cafe.name, "cafe created");
if is_datastar_request(&headers) { if is_datastar_request(&headers) {
render_cafe_list_fragment(state, request, search, true) render_cafe_list_fragment(state, request, search, true)
.await .await
@ -108,6 +111,7 @@ pub(crate) async fn update_cafe(
.update(id, payload) .update(id, payload)
.await .await
.map_err(AppError::from)?; .map_err(AppError::from)?;
info!(%id, "cafe updated");
Ok(Json(cafe)) Ok(Json(cafe))
} }

View file

@ -15,6 +15,7 @@ use crate::domain::cafes::NewCafe;
use crate::domain::cups::NewCup; use crate::domain::cups::NewCup;
use crate::domain::ids::{CafeId, RoastId}; use crate::domain::ids::{CafeId, RoastId};
use crate::presentation::web::templates::CheckInTemplate; use crate::presentation::web::templates::CheckInTemplate;
use tracing::info;
#[tracing::instrument(skip(state, cookies))] #[tracing::instrument(skip(state, cookies))]
pub(crate) async fn checkin_page( pub(crate) async fn checkin_page(
@ -115,6 +116,8 @@ pub(crate) async fn submit_checkin(
.await .await
.map_err(AppError::from)?; .map_err(AppError::from)?;
info!(cup_id = %cup.id, %cafe_id, "check-in recorded");
if is_datastar_request(&headers) { if is_datastar_request(&headers) {
crate::application::routes::support::render_signals_json(&[]).map_err(ApiError::from) crate::application::routes::support::render_signals_json(&[]).map_err(ApiError::from)
} else if matches!(source, PayloadSource::Form) { } else if matches!(source, PayloadSource::Form) {

View file

@ -17,6 +17,7 @@ use crate::domain::ids::CupId;
use crate::domain::listing::{ListRequest, SortDirection}; use crate::domain::listing::{ListRequest, SortDirection};
use crate::presentation::web::templates::CupListTemplate; use crate::presentation::web::templates::CupListTemplate;
use crate::presentation::web::views::{CupView, ListNavigator, Paginated}; use crate::presentation::web::views::{CupView, ListNavigator, Paginated};
use tracing::info;
const CUP_PAGE_PATH: &str = "/data?type=cups"; const CUP_PAGE_PATH: &str = "/data?type=cups";
const CUP_FRAGMENT_PATH: &str = "/data?type=cups#cup-list"; const CUP_FRAGMENT_PATH: &str = "/data?type=cups#cup-list";
@ -60,6 +61,8 @@ pub(crate) async fn create_cup(
.await .await
.map_err(AppError::from)?; .map_err(AppError::from)?;
info!(cup_id = %cup.id, "cup created");
if is_datastar_request(&headers) { if is_datastar_request(&headers) {
render_cup_list_fragment(state, request, search, true) render_cup_list_fragment(state, request, search, true)
.await .await

View file

@ -5,6 +5,7 @@ use axum::extract::{Path, Query, State};
use axum::http::{HeaderMap, StatusCode}; use axum::http::{HeaderMap, StatusCode};
use axum::response::{IntoResponse, Redirect, Response}; use axum::response::{IntoResponse, Redirect, Response};
use serde::Deserialize; use serde::Deserialize;
use tracing::{info, warn};
use super::macros::{define_delete_handler, define_get_handler, define_list_fragment_renderer}; use super::macros::{define_delete_handler, define_get_handler, define_list_fragment_renderer};
use crate::application::auth::AuthenticatedUser; use crate::application::auth::AuthenticatedUser;
@ -63,6 +64,8 @@ pub(crate) async fn create_gear(
.await .await
.map_err(AppError::from)?; .map_err(AppError::from)?;
info!(gear_id = %gear.id, make = %gear.make, model = %gear.model, "gear created");
// Add timeline event // Add timeline event
let event = NewTimelineEvent { let event = NewTimelineEvent {
entity_type: "gear".to_string(), entity_type: "gear".to_string(),
@ -89,7 +92,9 @@ pub(crate) async fn create_gear(
roaster_slug: None, // Gear is not related to roasters roaster_slug: None, // Gear is not related to roasters
brew_data: None, 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) { if is_datastar_request(&headers) {
render_gear_list_fragment(state, request, search, true) render_gear_list_fragment(state, request, search, true)
@ -145,6 +150,8 @@ pub(crate) async fn update_gear(
.await .await
.map_err(AppError::from)?; .map_err(AppError::from)?;
info!(%id, "gear updated");
if is_datastar_request(&headers) { if is_datastar_request(&headers) {
render_gear_list_fragment(state, request, search, true) render_gear_list_fragment(state, request, search, true)
.await .await

View file

@ -95,6 +95,8 @@ macro_rules! define_delete_handler {
.await .await
.map_err(crate::application::errors::AppError::from)?; .map_err(crate::application::errors::AppError::from)?;
tracing::info!(%id, "entity deleted");
if crate::application::routes::support::is_datastar_request(&headers) { if crate::application::routes::support::is_datastar_request(&headers) {
$render_fragment(state, request, search, true) $render_fragment(state, request, search, true)
.await .await

View file

@ -16,6 +16,7 @@ use crate::domain::roasters::{NewRoaster, Roaster, RoasterSortKey, UpdateRoaster
use crate::infrastructure::ai::{self, ExtractionInput}; use crate::infrastructure::ai::{self, ExtractionInput};
use crate::presentation::web::templates::RoasterListTemplate; use crate::presentation::web::templates::RoasterListTemplate;
use crate::presentation::web::views::{ListNavigator, Paginated, RoasterView}; use crate::presentation::web::views::{ListNavigator, Paginated, RoasterView};
use tracing::info;
const ROASTER_PAGE_PATH: &str = "/data?type=roasters"; const ROASTER_PAGE_PATH: &str = "/data?type=roasters";
const ROASTER_FRAGMENT_PATH: &str = "/data?type=roasters#roaster-list"; const ROASTER_FRAGMENT_PATH: &str = "/data?type=roasters#roaster-list";
@ -71,6 +72,8 @@ pub(crate) async fn create_roaster(
.await .await
.map_err(AppError::from)?; .map_err(AppError::from)?;
info!(roaster_id = %roaster.id, name = %roaster.name, "roaster created");
if is_datastar_request(&headers) { if is_datastar_request(&headers) {
render_roaster_list_fragment(state, request, search, true) render_roaster_list_fragment(state, request, search, true)
.await .await
@ -107,6 +110,7 @@ pub(crate) async fn update_roaster(
.update(id, payload) .update(id, payload)
.await .await
.map_err(AppError::from)?; .map_err(AppError::from)?;
info!(%id, "roaster updated");
Ok(Json(roaster)) Ok(Json(roaster))
} }

View file

@ -19,6 +19,7 @@ use crate::domain::roasts::{NewRoast, RoastSortKey, RoastWithRoaster, UpdateRoas
use crate::infrastructure::ai::{self, ExtractionInput}; use crate::infrastructure::ai::{self, ExtractionInput};
use crate::presentation::web::templates::{RoastListTemplate, RoastOptionsTemplate}; use crate::presentation::web::templates::{RoastListTemplate, RoastOptionsTemplate};
use crate::presentation::web::views::{ListNavigator, Paginated, RoastView}; use crate::presentation::web::views::{ListNavigator, Paginated, RoastView};
use tracing::info;
const ROAST_PAGE_PATH: &str = "/data?type=roasts"; const ROAST_PAGE_PATH: &str = "/data?type=roasts";
const ROAST_FRAGMENT_PATH: &str = "/data?type=roasts#roast-list"; const ROAST_FRAGMENT_PATH: &str = "/data?type=roasts#roast-list";
@ -69,6 +70,8 @@ pub(crate) async fn create_roast(
.await .await
.map_err(AppError::from)?; .map_err(AppError::from)?;
info!(roast_id = %roast.id, name = %roast.name, "roast created");
if is_datastar_request(&headers) { if is_datastar_request(&headers) {
render_roast_list_fragment(state, request, search, true) render_roast_list_fragment(state, request, search, true)
.await .await
@ -169,6 +172,8 @@ pub(crate) async fn update_roast(
.await .await
.map_err(AppError::from)?; .map_err(AppError::from)?;
info!(%id, "roast updated");
let enriched = state let enriched = state
.roast_repo .roast_repo
.get_with_roaster(id) .get_with_roaster(id)

View file

@ -3,6 +3,7 @@ use axum::extract::State;
use axum::http::{HeaderMap, StatusCode}; use axum::http::{HeaderMap, StatusCode};
use axum::response::{IntoResponse, Response}; use axum::response::{IntoResponse, Response};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tracing::{info, warn};
use crate::application::auth::AuthenticatedUser; use crate::application::auth::AuthenticatedUser;
use crate::application::errors::{ApiError, AppError}; use crate::application::errors::{ApiError, AppError};
@ -285,6 +286,8 @@ pub(crate) async fn submit_scan(
.await .await
.map_err(AppError::from)?; .map_err(AppError::from)?;
info!(roaster_id = %roaster.id, roast_id = %roast.id, roast_name = %roast.name, "scan created roast");
// Optionally create a bag // Optionally create a bag
let wants_bag = submission let wants_bag = submission
.open_bag .open_bag
@ -324,7 +327,9 @@ pub(crate) async fn submit_scan(
roaster_slug: Some(roaster.slug.clone()), roaster_slug: Some(roaster.slug.clone()),
brew_data: None, 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); let redirect = format!("/roasters/{}/roasts/{}", roaster.slug, roast.slug);