@@ -963,6 +963,10 @@ enum WebhookDeliveryCommands {
963
963
/// List webhook deliveries
964
964
#[ clap( alias = "ls" ) ]
965
965
List ( WebhookDeliveryListArgs ) ,
966
+
967
+ /// Show details on a webhook delivery, including its payload and attempt history.
968
+ #[ clap( alias = "show" ) ]
969
+ Info ( WebhookDeliveryInfoArgs ) ,
966
970
}
967
971
968
972
#[ derive( Debug , Args , Clone ) ]
@@ -992,6 +996,12 @@ struct WebhookDeliveryListArgs {
992
996
after : Option < DateTime < Utc > > ,
993
997
}
994
998
999
+ #[ derive( Debug , Args , Clone ) ]
1000
+ struct WebhookDeliveryInfoArgs {
1001
+ /// The ID of the delivery to show.
1002
+ delivery_id : Uuid ,
1003
+ }
1004
+
995
1005
impl DbArgs {
996
1006
/// Run a `omdb db` subcommand.
997
1007
///
@@ -6967,6 +6977,9 @@ async fn cmd_db_webhook(
6967
6977
WebhookCommands :: Delivery {
6968
6978
command : WebhookDeliveryCommands :: List ( args) ,
6969
6979
} => 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 ,
6970
6983
WebhookCommands :: Event => {
6971
6984
Err ( anyhow:: anyhow!( "not yet implemented, sorry!" ) )
6972
6985
}
@@ -7285,6 +7298,7 @@ async fn cmd_db_webhook_delivery_list(
7285
7298
check_limit ( & deliveries, fetch_opts. fetch_limit , ctx) ;
7286
7299
7287
7300
#[ derive( Tabled ) ]
7301
+ #[ tabled( rename_all = "SCREAMING_SNAKE_CASE" ) ]
7288
7302
struct DeliveryRow {
7289
7303
id : Uuid ,
7290
7304
trigger : nexus_db_model:: WebhookDeliveryTrigger ,
@@ -7297,13 +7311,15 @@ async fn cmd_db_webhook_delivery_list(
7297
7311
}
7298
7312
7299
7313
#[ derive( Tabled ) ]
7314
+ #[ tabled( rename_all = "SCREAMING_SNAKE_CASE" ) ]
7300
7315
struct WithEventId < T : Tabled > {
7301
7316
#[ tabled( inline) ]
7302
7317
inner : T ,
7303
7318
event_id : Uuid ,
7304
7319
}
7305
7320
7306
7321
#[ derive( Tabled ) ]
7322
+ #[ tabled( rename_all = "SCREAMING_SNAKE_CASE" ) ]
7307
7323
struct WithRxId < T : Tabled > {
7308
7324
#[ tabled( inline) ]
7309
7325
inner : T ,
@@ -7418,6 +7434,177 @@ async fn lookup_webhook_rx(
7418
7434
. with_context ( || format ! ( "loading webhook_receiver {name_or_id}" ) )
7419
7435
}
7420
7436
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
+
7421
7608
// Format a `chrono::DateTime` in RFC3339 with milliseconds precision and using
7422
7609
// `Z` rather than the UTC offset for UTC timestamps, to save a few characters
7423
7610
// of line width in tabular output.
@@ -7432,3 +7619,11 @@ fn datetime_opt_rfc3339_concise(t: &Option<DateTime<Utc>>) -> String {
7432
7619
t. map ( |t| t. to_rfc3339_opts ( chrono:: format:: SecondsFormat :: Millis , true ) )
7433
7620
. unwrap_or_else ( || "-" . to_string ( ) )
7434
7621
}
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