@@ -963,6 +963,10 @@ enum WebhookDeliveryCommands {
963963 /// List webhook deliveries
964964 #[ clap( alias = "ls" ) ]
965965 List ( WebhookDeliveryListArgs ) ,
966+
967+ /// Show details on a webhook delivery, including its payload and attempt history.
968+ #[ clap( alias = "show" ) ]
969+ Info ( WebhookDeliveryInfoArgs ) ,
966970}
967971
968972#[ derive( Debug , Args , Clone ) ]
@@ -992,6 +996,12 @@ struct WebhookDeliveryListArgs {
992996 after : Option < DateTime < Utc > > ,
993997}
994998
999+ #[ derive( Debug , Args , Clone ) ]
1000+ struct WebhookDeliveryInfoArgs {
1001+ /// The ID of the delivery to show.
1002+ delivery_id : Uuid ,
1003+ }
1004+
9951005impl DbArgs {
9961006 /// Run a `omdb db` subcommand.
9971007 ///
@@ -6967,6 +6977,9 @@ async fn cmd_db_webhook(
69676977 WebhookCommands :: Delivery {
69686978 command : WebhookDeliveryCommands :: List ( args) ,
69696979 } => cmd_db_webhook_delivery_list ( datastore, fetch_opts, args) . await ,
6980+ WebhookCommands :: Delivery {
6981+ command : WebhookDeliveryCommands :: Info ( args) ,
6982+ } => cmd_db_webhook_delivery_info ( datastore, fetch_opts, args) . await ,
69706983 WebhookCommands :: Event => {
69716984 Err ( anyhow:: anyhow!( "not yet implemented, sorry!" ) )
69726985 }
@@ -7285,6 +7298,7 @@ async fn cmd_db_webhook_delivery_list(
72857298 check_limit ( & deliveries, fetch_opts. fetch_limit , ctx) ;
72867299
72877300 #[ derive( Tabled ) ]
7301+ #[ tabled( rename_all = "SCREAMING_SNAKE_CASE" ) ]
72887302 struct DeliveryRow {
72897303 id : Uuid ,
72907304 trigger : nexus_db_model:: WebhookDeliveryTrigger ,
@@ -7297,13 +7311,15 @@ async fn cmd_db_webhook_delivery_list(
72977311 }
72987312
72997313 #[ derive( Tabled ) ]
7314+ #[ tabled( rename_all = "SCREAMING_SNAKE_CASE" ) ]
73007315 struct WithEventId < T : Tabled > {
73017316 #[ tabled( inline) ]
73027317 inner : T ,
73037318 event_id : Uuid ,
73047319 }
73057320
73067321 #[ derive( Tabled ) ]
7322+ #[ tabled( rename_all = "SCREAMING_SNAKE_CASE" ) ]
73077323 struct WithRxId < T : Tabled > {
73087324 #[ tabled( inline) ]
73097325 inner : T ,
@@ -7418,6 +7434,177 @@ async fn lookup_webhook_rx(
74187434 . with_context ( || format ! ( "loading webhook_receiver {name_or_id}" ) )
74197435}
74207436
7437+ async fn cmd_db_webhook_delivery_info (
7438+ datastore : & DataStore ,
7439+ fetch_opts : & DbFetchOptions ,
7440+ args : & WebhookDeliveryInfoArgs ,
7441+ ) -> anyhow:: Result < ( ) > {
7442+ use db:: model:: WebhookDeliveryAttempt ;
7443+ use db:: model:: schema:: webhook_delivery:: dsl;
7444+ use db:: model:: schema:: webhook_delivery_attempt:: dsl as attempt_dsl;
7445+
7446+ let WebhookDeliveryInfoArgs { delivery_id } = args;
7447+ let conn = datastore. pool_connection_for_tests ( ) . await ?;
7448+ let delivery = dsl:: webhook_delivery
7449+ . filter ( dsl:: id. eq ( * delivery_id) )
7450+ . limit ( 1 )
7451+ . select ( WebhookDelivery :: as_select ( ) )
7452+ . get_result_async ( & * conn)
7453+ . await
7454+ . optional ( )
7455+ . with_context ( || format ! ( "loading webhook delivery {delivery_id}" ) ) ?
7456+ . ok_or_else ( || {
7457+ anyhow:: anyhow!( "no webhook delivery {delivery_id} exists" )
7458+ } ) ?;
7459+
7460+ const ID : & ' static str = "ID" ;
7461+ const EVENT_ID : & ' static str = "event ID" ;
7462+ const RECEIVER_ID : & ' static str = "receiver ID" ;
7463+ const STATE : & ' static str = "state" ;
7464+ const TRIGGER : & ' static str = "triggered by" ;
7465+ const ATTEMPTS : & ' static str = "attempts" ;
7466+ const TIME_CREATED : & ' static str = "created at" ;
7467+ const TIME_COMPLETED : & ' static str = "completed at" ;
7468+
7469+ const DELIVERATOR_ID : & ' static str = "by Nexus" ;
7470+ const TIME_DELIVERY_STARTED : & ' static str = "started at" ;
7471+
7472+ const WIDTH : usize = const_max_len ( & [
7473+ ID ,
7474+ EVENT_ID ,
7475+ RECEIVER_ID ,
7476+ TRIGGER ,
7477+ STATE ,
7478+ TIME_CREATED ,
7479+ TIME_COMPLETED ,
7480+ DELIVERATOR_ID ,
7481+ TIME_DELIVERY_STARTED ,
7482+ ATTEMPTS ,
7483+ ] ) ;
7484+
7485+ let WebhookDelivery {
7486+ id,
7487+ event_id,
7488+ rx_id,
7489+ trigger,
7490+ payload,
7491+ attempts,
7492+ time_created,
7493+ time_completed,
7494+ state,
7495+ deliverator_id,
7496+ time_delivery_started,
7497+ } = delivery;
7498+ println ! ( "\n {:=<80}" , "== DELIVERY " ) ;
7499+ println ! ( " {ID:>WIDTH$}: {id}" ) ;
7500+ println ! ( " {EVENT_ID:>WIDTH$}: {event_id}" ) ;
7501+ println ! ( " {RECEIVER_ID:>WIDTH$}: {rx_id}" ) ;
7502+ println ! ( " {STATE:>WIDTH$}: {state}" ) ;
7503+ println ! ( " {TRIGGER:>WIDTH$}: {trigger}" ) ;
7504+ println ! ( " {TIME_CREATED:>WIDTH$}: {time_created}" ) ;
7505+ println ! ( " {ATTEMPTS}: {}" , attempts. 0 ) ;
7506+
7507+ if let Some ( completed) = time_completed {
7508+ println ! ( "\n {:=<80}" , "== DELIVERY COMPLETED " ) ;
7509+ println ! ( " {TIME_COMPLETED:>WIDTH$}: {completed}" ) ;
7510+ if let Some ( started) = time_delivery_started {
7511+ println ! ( " {TIME_DELIVERY_STARTED:>WIDTH$}: {started}" ) ;
7512+ } else {
7513+ println ! (
7514+ "/!\\ WEIRD: delivery is completed but has no start timestamp?"
7515+ ) ;
7516+ }
7517+ if let Some ( nexus) = deliverator_id {
7518+ println ! ( " {DELIVERATOR_ID:>WIDTH$}: {nexus}" ) ;
7519+ } else {
7520+ println ! ( "/!\\ WEIRD: delivery is completed but has no Nexus ID?" ) ;
7521+ }
7522+ } else if let Some ( started) = time_delivery_started {
7523+ println ! ( "\n {:=<80}" , "== DELIVERY IN PROGRESS " ) ;
7524+ println ! ( " {TIME_DELIVERY_STARTED:>WIDTH$}: {started}" ) ;
7525+
7526+ if let Some ( nexus) = deliverator_id {
7527+ println ! ( " {DELIVERATOR_ID:>WIDTH$}: {nexus}" ) ;
7528+ } else {
7529+ println ! (
7530+ "/!\\ WEIRD: delivery is in progress but has no Nexus ID?"
7531+ ) ;
7532+ }
7533+ } else if let Some ( deliverator) = deliverator_id {
7534+ println ! (
7535+ "/!\\ WEIRD: delivery is not completed or in progress but has \
7536+ Nexus ID {deliverator:?}"
7537+ ) ;
7538+ }
7539+
7540+ println ! ( "\n {:=<80}" , "== JSON PAYLOAD " ) ;
7541+ match serde_json:: to_string_pretty ( & payload) {
7542+ Ok ( payload_str) => println ! ( "{payload_str}" ) ,
7543+ Err ( e) => eprintln ! (
7544+ "/!\\ payload JSON did not serialize: {e}\n payload: {payload:?}" ,
7545+ ) ,
7546+ }
7547+
7548+ // Okay, now go get attempts for this delivery.
7549+ let ctx = || format ! ( "listing delivery attempts for {delivery_id}" ) ;
7550+ let attempts = attempt_dsl:: webhook_delivery_attempt
7551+ . filter ( attempt_dsl:: delivery_id. eq ( * delivery_id) )
7552+ . order_by ( attempt_dsl:: attempt. desc ( ) )
7553+ . limit ( fetch_opts. fetch_limit . get ( ) . into ( ) )
7554+ . select ( WebhookDeliveryAttempt :: as_select ( ) )
7555+ . load_async ( & * conn)
7556+ . await
7557+ . with_context ( ctx) ?;
7558+
7559+ check_limit ( & attempts, fetch_opts. fetch_limit , ctx) ;
7560+
7561+ if !attempts. is_empty ( ) {
7562+ println ! ( "\n {:=<80}" , "== DELIVERY ATTEMPT HISTORY " ) ;
7563+
7564+ #[ derive( Tabled ) ]
7565+ #[ tabled( rename_all = "SCREAMING_SNAKE_CASE" ) ]
7566+ struct DeliveryAttemptRow {
7567+ #[ tabled( rename = "#" ) ]
7568+ attempt : u8 ,
7569+ #[ tabled( display_with = "datetime_rfc3339_concise" ) ]
7570+ time_created : DateTime < Utc > ,
7571+ nexus_id : Uuid ,
7572+ result : db:: model:: WebhookDeliveryAttemptResult ,
7573+ #[ tabled( display_with = "display_i16_opt" ) ]
7574+ status : Option < i16 > ,
7575+ #[ tabled( display_with = "display_time_delta_opt" ) ]
7576+ duration : Option < chrono:: TimeDelta > ,
7577+ }
7578+
7579+ let rows = attempts. into_iter ( ) . map (
7580+ |WebhookDeliveryAttempt {
7581+ delivery_id : _,
7582+ rx_id : _,
7583+ attempt,
7584+ result,
7585+ response_status,
7586+ response_duration,
7587+ time_created,
7588+ deliverator_id,
7589+ } | DeliveryAttemptRow {
7590+ attempt : attempt. 0 ,
7591+ time_created,
7592+ nexus_id : deliverator_id. into_untyped_uuid ( ) ,
7593+ result,
7594+ status : response_status,
7595+ duration : response_duration,
7596+ } ,
7597+ ) ;
7598+ let mut table = tabled:: Table :: new ( rows) ;
7599+ table
7600+ . with ( tabled:: settings:: Style :: empty ( ) )
7601+ . with ( tabled:: settings:: Padding :: new ( 0 , 1 , 0 , 0 ) ) ;
7602+ println ! ( "{table}" ) ;
7603+ }
7604+
7605+ Ok ( ( ) )
7606+ }
7607+
74217608// Format a `chrono::DateTime` in RFC3339 with milliseconds precision and using
74227609// `Z` rather than the UTC offset for UTC timestamps, to save a few characters
74237610// of line width in tabular output.
@@ -7432,3 +7619,11 @@ fn datetime_opt_rfc3339_concise(t: &Option<DateTime<Utc>>) -> String {
74327619 t. map ( |t| t. to_rfc3339_opts ( chrono:: format:: SecondsFormat :: Millis , true ) )
74337620 . unwrap_or_else ( || "-" . to_string ( ) )
74347621}
7622+
7623+ fn display_time_delta_opt ( t : & Option < chrono:: TimeDelta > ) -> String {
7624+ t. map ( |t| t. to_string ( ) ) . unwrap_or_else ( || "-" . to_string ( ) )
7625+ }
7626+
7627+ fn display_i16_opt ( u : & Option < i16 > ) -> String {
7628+ u. map ( |u| u. to_string ( ) ) . unwrap_or_else ( || "-" . to_string ( ) )
7629+ }
0 commit comments