From 9258844cbfa88a343ca8e48882c994e9f00c9464 Mon Sep 17 00:00:00 2001 From: Nick Patilsen Date: Wed, 12 Nov 2025 01:16:11 +0000 Subject: [PATCH 1/9] refactor: return shutdown sender from Otel::new() as tuple - Change Otel::new() to return (Otel, mpsc::Sender<()>) - Remove redundant shutdown methods, use channel directly - Centralize graceful shutdown logic in run() method - Update sample app and tests to use tuple return pattern - Add target/ directories to .gitignore --- .gitignore | 3 +++ examples/sample-app/src/main.rs | 5 +++-- src/lib.rs | 40 ++++++++++++++++----------------- tests/integration_test.rs | 2 +- 4 files changed, 26 insertions(+), 24 deletions(-) diff --git a/.gitignore b/.gitignore index ce33184..5cddde3 100644 --- a/.gitignore +++ b/.gitignore @@ -397,5 +397,8 @@ FodyWeavers.xsd # JetBrains Rider *.sln.iml +# Cargo build artifacts +target/ + # Cargo lock Cargo.lock diff --git a/examples/sample-app/src/main.rs b/examples/sample-app/src/main.rs index e48ac74..ba87be5 100644 --- a/examples/sample-app/src/main.rs +++ b/examples/sample-app/src/main.rs @@ -41,6 +41,7 @@ async fn main() { interval_secs: 1, timeout: 5, export_severity: Some(Severity::Error), + target_filters: None, ca_cert_path: args.ca_cert_path, bearer_token_provider_fn: Some(get_dummy_bearer_token), }]; @@ -64,7 +65,7 @@ async fn main() { ..Config::default() }; - let mut otel_component = Otel::new(config); + let (mut otel_component, shutdown_tx) = Otel::new(config); // Start the otel running task let otel_long_running_task = otel_component.run(); // initialize static metrics @@ -95,7 +96,7 @@ async fn main() { }); let _ = join!(instrumentation_task, otel_long_running_task); - otel_component.shutdown().await; + let _ = shutdown_tx.send(()).await; } #[derive(Parser, Debug)] diff --git a/src/lib.rs b/src/lib.rs index 2cc0cef..9100b3b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -56,13 +56,12 @@ pub struct Otel { meter_provider: SdkMeterProvider, logger_provider: Option, ca_cert_paths: HashSet, - shutdown_tx: mpsc::Sender<()>, shutdown_rx: mpsc::Receiver<()>, } impl Otel { #[must_use] - pub fn new(config: Config) -> Otel { + pub fn new(config: Config) -> (Otel, mpsc::Sender<()>) { let logger_provider = match loggers::init_logs(config.clone()) { Ok(logger_provider) => Some(logger_provider), Err(e) => { @@ -91,15 +90,17 @@ impl Otel { } let (shutdown_tx, shutdown_rx) = mpsc::channel(1); + let shutdown_tx_clone = shutdown_tx.clone(); - Otel { + let otel = Otel { registry, meter_provider, logger_provider, ca_cert_paths, - shutdown_tx, shutdown_rx, - } + }; + + (otel, shutdown_tx_clone) } /// Long running tasks for otel propagation. @@ -144,28 +145,25 @@ impl Otel { return Err(OtelError::PrometheusServerStopped) } _ = self.shutdown_rx.recv() => { + // Graceful shutdown that flushes any pending metrics and logs to the exporter. info!("shutting down otel component"); - } - } - Ok(()) - } + if let Err(metrics_error) = self.meter_provider.force_flush() { + warn!("ecountered error while flushing metrics: {metrics_error:?}"); + } + if let Err(metrics_error) = self.meter_provider.shutdown() { + warn!("ecountered error while shutting down meter provider: {metrics_error:?}"); + } - /// Graceful shutdown that flushes any pending metrics and logs to the exporter. - pub async fn shutdown(&self) { - if let Err(metrics_error) = self.meter_provider.force_flush() { - warn!("ecountered error while flushing metrics: {metrics_error:?}"); - } - if let Err(metrics_error) = self.meter_provider.shutdown() { - warn!("ecountered error while shutting down meter provider: {metrics_error:?}"); - } + if let Some(logger_provider) = self.logger_provider.clone() { + logger_provider.force_flush(); + let _ = logger_provider.shutdown(); + } - if let Some(logger_provider) = self.logger_provider.clone() { - logger_provider.force_flush(); - let _ = logger_provider.shutdown(); + } } - let _ = self.shutdown_tx.send(()).await; + Ok(()) } } diff --git a/tests/integration_test.rs b/tests/integration_test.rs index 2a32423..732e7ec 100644 --- a/tests/integration_test.rs +++ b/tests/integration_test.rs @@ -183,7 +183,7 @@ async fn end_to_end_test() { ..Config::default() }; - let mut otel_component = Otel::new(config); + let (mut otel_component, _shutdown_tx) = Otel::new(config); let otel_long_running_task = tokio::spawn(async move { otel_component.run().await }); let run_tests_task = run_tests( filtered_target.metrics_rx, From 65f29ff82c7ee3f6c0dc344ea1638cee2ae69d50 Mon Sep 17 00:00:00 2001 From: Nick Patilsen Date: Wed, 12 Nov 2025 19:33:40 +0000 Subject: [PATCH 2/9] keep shutdown sender as part of struct --- src/lib.rs | 49 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/src/lib.rs b/src/lib.rs index 9100b3b..e32cc36 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -56,6 +56,7 @@ pub struct Otel { meter_provider: SdkMeterProvider, logger_provider: Option, ca_cert_paths: HashSet, + shutdown_tx: mpsc::Sender<()>, shutdown_rx: mpsc::Receiver<()>, } @@ -97,6 +98,7 @@ impl Otel { meter_provider, logger_provider, ca_cert_paths, + shutdown_tx, shutdown_rx, }; @@ -165,6 +167,10 @@ impl Otel { Ok(()) } + + pub async fn shutdown(&self) -> Result<(), mpsc::error::SendError<()>> { + self.shutdown_tx.send(()).await // Just sends signal, run() handles the rest + } } #[allow(clippy::type_complexity)] @@ -506,3 +512,46 @@ impl Interceptor for AuthIntercepter { Ok(modified_request) } } + +#[cfg(test)] +mod tests { + use super::*; + use std::time::Duration; + use tokio::time::timeout; + + #[tokio::test] + async fn test_shutdown_approaches_both_work() { + // Test that both shutdown approaches trigger the same graceful shutdown behavior + { + // Arrange + let config = Config::default(); + let (mut otel, shutdown_tx) = Otel::new(config); + + let run_task = tokio::spawn(async move { + otel.run().await + }); + + tokio::time::sleep(Duration::from_millis(50)).await; + + // Act + shutdown_tx.send(()).await.expect("Should be able to send shutdown signal"); + + // Assert + let result = timeout(Duration::from_secs(1), run_task).await; + assert!(result.is_ok(), "Run task should complete after direct shutdown"); + assert!(result.unwrap().is_ok(), "Run task should exit cleanly"); + } + + { + // Arrange + let config = Config::default(); + let (otel, _shutdown_tx) = Otel::new(config); + + // Act + let shutdown_result = otel.shutdown().await; + + // Assert + assert!(shutdown_result.is_ok(), "Convenience shutdown should work"); + } + } +} From c9d896721820b33998342175f1ca20cb6972aa3f Mon Sep 17 00:00:00 2001 From: Nick Patilsen Date: Wed, 12 Nov 2025 19:35:20 +0000 Subject: [PATCH 3/9] fmt --- src/lib.rs | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index e32cc36..6be57f3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -169,7 +169,7 @@ impl Otel { } pub async fn shutdown(&self) -> Result<(), mpsc::error::SendError<()>> { - self.shutdown_tx.send(()).await // Just sends signal, run() handles the rest + self.shutdown_tx.send(()).await // Just sends signal, run() handles the rest } } @@ -526,27 +526,31 @@ mod tests { // Arrange let config = Config::default(); let (mut otel, shutdown_tx) = Otel::new(config); - - let run_task = tokio::spawn(async move { - otel.run().await - }); - + + let run_task = tokio::spawn(async move { otel.run().await }); + tokio::time::sleep(Duration::from_millis(50)).await; - + // Act - shutdown_tx.send(()).await.expect("Should be able to send shutdown signal"); - + shutdown_tx + .send(()) + .await + .expect("Should be able to send shutdown signal"); + // Assert let result = timeout(Duration::from_secs(1), run_task).await; - assert!(result.is_ok(), "Run task should complete after direct shutdown"); + assert!( + result.is_ok(), + "Run task should complete after direct shutdown" + ); assert!(result.unwrap().is_ok(), "Run task should exit cleanly"); } - + { // Arrange - let config = Config::default(); + let config = Config::default(); let (otel, _shutdown_tx) = Otel::new(config); - + // Act let shutdown_result = otel.shutdown().await; From 3080fe571f74182058469a1d7a0fb90c350fb6d1 Mon Sep 17 00:00:00 2001 From: Nick Patilsen Date: Wed, 12 Nov 2025 19:40:19 +0000 Subject: [PATCH 4/9] doc comments --- src/lib.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/lib.rs b/src/lib.rs index 6be57f3..df9debf 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -168,6 +168,10 @@ impl Otel { Ok(()) } + /// Convenience function to trigger shutdown from the Otel struct directly. + /// + /// # Errors + /// * `mpsc::error::SendError` - If the shutdown signal could not be sent pub async fn shutdown(&self) -> Result<(), mpsc::error::SendError<()>> { self.shutdown_tx.send(()).await // Just sends signal, run() handles the rest } From d07eb3d06bfb07d4f62aa9bd9b0651b8ca77ab43 Mon Sep 17 00:00:00 2001 From: Nick Patilsen Date: Wed, 12 Nov 2025 22:22:51 +0000 Subject: [PATCH 5/9] api to access shutdown handler --- examples/sample-app/src/main.rs | 4 +- src/lib.rs | 87 +++++++++++++++++++++++++++++---- tests/integration_test.rs | 2 +- 3 files changed, 81 insertions(+), 12 deletions(-) diff --git a/examples/sample-app/src/main.rs b/examples/sample-app/src/main.rs index ba87be5..d3c95f9 100644 --- a/examples/sample-app/src/main.rs +++ b/examples/sample-app/src/main.rs @@ -65,7 +65,7 @@ async fn main() { ..Config::default() }; - let (mut otel_component, shutdown_tx) = Otel::new(config); + let (mut otel_component, shutdown_handle) = Otel::with_shutdown_handle(config); // Start the otel running task let otel_long_running_task = otel_component.run(); // initialize static metrics @@ -96,7 +96,7 @@ async fn main() { }); let _ = join!(instrumentation_task, otel_long_running_task); - let _ = shutdown_tx.send(()).await; + let _ = shutdown_handle.shutdown().await; } #[derive(Parser, Debug)] diff --git a/src/lib.rs b/src/lib.rs index df9debf..c30a570 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -60,9 +60,32 @@ pub struct Otel { shutdown_rx: mpsc::Receiver<()>, } +pub struct ShutdownHandle { + sender: mpsc::Sender<()>, +} + +impl ShutdownHandle { + /// Send a shutdown signal + pub async fn shutdown(&self) -> Result<(), mpsc::error::SendError<()>> { + self.sender.send(()).await + } + + /// Get a clone of the shutdown sender for use in other contexts + pub fn sender(&self) -> mpsc::Sender<()> { + self.sender.clone() + } + + /// Clone shutdown handle to share shutdown capability + pub fn clone_handle(&self) -> Self { + Self { + sender: self.sender.clone(), + } + } +} + impl Otel { #[must_use] - pub fn new(config: Config) -> (Otel, mpsc::Sender<()>) { + pub fn new(config: Config) -> Otel { let logger_provider = match loggers::init_logs(config.clone()) { Ok(logger_provider) => Some(logger_provider), Err(e) => { @@ -91,18 +114,23 @@ impl Otel { } let (shutdown_tx, shutdown_rx) = mpsc::channel(1); - let shutdown_tx_clone = shutdown_tx.clone(); - let otel = Otel { + Otel { registry, meter_provider, logger_provider, ca_cert_paths, shutdown_tx, shutdown_rx, - }; + } + } - (otel, shutdown_tx_clone) + /// Alternative constructor that also returns a shutdown handle. + #[must_use] + pub fn with_shutdown_handle(config: Config) -> (Otel, ShutdownHandle) { + let otel = Self::new(config); + let handle = otel.shutdown_handle(); + (otel, handle) } /// Long running tasks for otel propagation. @@ -168,6 +196,13 @@ impl Otel { Ok(()) } + /// Get a shutdown handle that can be used to trigger shutdown from other contexts. + pub fn shutdown_handle(&self) -> ShutdownHandle { + ShutdownHandle { + sender: self.shutdown_tx.clone(), + } + } + /// Convenience function to trigger shutdown from the Otel struct directly. /// /// # Errors @@ -529,15 +564,15 @@ mod tests { { // Arrange let config = Config::default(); - let (mut otel, shutdown_tx) = Otel::new(config); + let (mut otel, shutdown_handle) = Otel::with_shutdown_handle(config); let run_task = tokio::spawn(async move { otel.run().await }); tokio::time::sleep(Duration::from_millis(50)).await; // Act - shutdown_tx - .send(()) + shutdown_handle + .shutdown() .await .expect("Should be able to send shutdown signal"); @@ -553,7 +588,7 @@ mod tests { { // Arrange let config = Config::default(); - let (otel, _shutdown_tx) = Otel::new(config); + let otel = Otel::new(config); // Act let shutdown_result = otel.shutdown().await; @@ -562,4 +597,38 @@ mod tests { assert!(shutdown_result.is_ok(), "Convenience shutdown should work"); } } + + #[tokio::test] + async fn test_both_constructor_patterns() { + // Pattern 1: Simple constructor - returns only Otel instance + { + let otel = Otel::new(Config::default()); + + // Get shutdown handle after construction + let handle1 = otel.shutdown_handle(); + let handle2 = otel.shutdown_handle(); // Can get multiple handles + + // Both handles should work + let _sender1 = handle1.sender(); + let _sender2 = handle2.sender(); + + // Direct shutdown also available + let _result = otel.shutdown().await; + } + + // Pattern 2: Constructor with explicit handle - returns tuple + { + let (otel, initial_handle) = Otel::with_shutdown_handle(Config::default()); + + // Can use the initial handle + let _sender1 = initial_handle.sender(); + + // Can still get more handles from the instance + let additional_handle = otel.shutdown_handle(); + let _sender2 = additional_handle.sender(); + + // All approaches available + let _result = otel.shutdown().await; + } + } } diff --git a/tests/integration_test.rs b/tests/integration_test.rs index 732e7ec..2a32423 100644 --- a/tests/integration_test.rs +++ b/tests/integration_test.rs @@ -183,7 +183,7 @@ async fn end_to_end_test() { ..Config::default() }; - let (mut otel_component, _shutdown_tx) = Otel::new(config); + let mut otel_component = Otel::new(config); let otel_long_running_task = tokio::spawn(async move { otel_component.run().await }); let run_tests_task = run_tests( filtered_target.metrics_rx, From b5c43ee2440db427de42b8be0114c9d3249e5398 Mon Sep 17 00:00:00 2001 From: Nick Patilsen Date: Wed, 12 Nov 2025 22:29:37 +0000 Subject: [PATCH 6/9] test pass --- src/lib.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/lib.rs b/src/lib.rs index c30a570..b8809b9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -66,16 +66,20 @@ pub struct ShutdownHandle { impl ShutdownHandle { /// Send a shutdown signal + /// # Errors + /// * `mpsc::error::SendError` - If the shutdown signal could not be sent pub async fn shutdown(&self) -> Result<(), mpsc::error::SendError<()>> { self.sender.send(()).await } /// Get a clone of the shutdown sender for use in other contexts + #[must_use] pub fn sender(&self) -> mpsc::Sender<()> { self.sender.clone() } /// Clone shutdown handle to share shutdown capability + #[must_use] pub fn clone_handle(&self) -> Self { Self { sender: self.sender.clone(), @@ -197,6 +201,7 @@ impl Otel { } /// Get a shutdown handle that can be used to trigger shutdown from other contexts. + #[must_use] pub fn shutdown_handle(&self) -> ShutdownHandle { ShutdownHandle { sender: self.shutdown_tx.clone(), From 7a2a2d8d73801ad267b8013b7ae2b13e984d0c58 Mon Sep 17 00:00:00 2001 From: Nick Patilsen Date: Thu, 13 Nov 2025 00:57:54 +0000 Subject: [PATCH 7/9] comments --- examples/sample-app/src/main.rs | 3 ++- src/lib.rs | 14 ++++---------- 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/examples/sample-app/src/main.rs b/examples/sample-app/src/main.rs index d3c95f9..58c92a7 100644 --- a/examples/sample-app/src/main.rs +++ b/examples/sample-app/src/main.rs @@ -65,7 +65,8 @@ async fn main() { ..Config::default() }; - let (mut otel_component, shutdown_handle) = Otel::with_shutdown_handle(config); + let mut otel_component = Otel::new(config); + let shutdown_handle = otel_component.shutdown_handle(); // Start the otel running task let otel_long_running_task = otel_component.run(); // initialize static metrics diff --git a/src/lib.rs b/src/lib.rs index b8809b9..ff95b7e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -129,14 +129,6 @@ impl Otel { } } - /// Alternative constructor that also returns a shutdown handle. - #[must_use] - pub fn with_shutdown_handle(config: Config) -> (Otel, ShutdownHandle) { - let otel = Self::new(config); - let handle = otel.shutdown_handle(); - (otel, handle) - } - /// Long running tasks for otel propagation. /// /// # Errors @@ -569,7 +561,8 @@ mod tests { { // Arrange let config = Config::default(); - let (mut otel, shutdown_handle) = Otel::with_shutdown_handle(config); + let mut otel = Otel::new(config); + let shutdown_handle = otel.shutdown_handle(); let run_task = tokio::spawn(async move { otel.run().await }); @@ -623,7 +616,8 @@ mod tests { // Pattern 2: Constructor with explicit handle - returns tuple { - let (otel, initial_handle) = Otel::with_shutdown_handle(Config::default()); + let otel = Otel::new(Config::default()); + let initial_handle = otel.shutdown_handle(); // Can use the initial handle let _sender1 = initial_handle.sender(); From 5c84ee7572f095d5aea0afabaae46a68399df030 Mon Sep 17 00:00:00 2001 From: Nick Patilsen Date: Thu, 13 Nov 2025 01:01:57 +0000 Subject: [PATCH 8/9] remove redundant fns --- src/lib.rs | 49 ------------------------------------------------- 1 file changed, 49 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index ff95b7e..1d3643f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -71,20 +71,6 @@ impl ShutdownHandle { pub async fn shutdown(&self) -> Result<(), mpsc::error::SendError<()>> { self.sender.send(()).await } - - /// Get a clone of the shutdown sender for use in other contexts - #[must_use] - pub fn sender(&self) -> mpsc::Sender<()> { - self.sender.clone() - } - - /// Clone shutdown handle to share shutdown capability - #[must_use] - pub fn clone_handle(&self) -> Self { - Self { - sender: self.sender.clone(), - } - } } impl Otel { @@ -595,39 +581,4 @@ mod tests { assert!(shutdown_result.is_ok(), "Convenience shutdown should work"); } } - - #[tokio::test] - async fn test_both_constructor_patterns() { - // Pattern 1: Simple constructor - returns only Otel instance - { - let otel = Otel::new(Config::default()); - - // Get shutdown handle after construction - let handle1 = otel.shutdown_handle(); - let handle2 = otel.shutdown_handle(); // Can get multiple handles - - // Both handles should work - let _sender1 = handle1.sender(); - let _sender2 = handle2.sender(); - - // Direct shutdown also available - let _result = otel.shutdown().await; - } - - // Pattern 2: Constructor with explicit handle - returns tuple - { - let otel = Otel::new(Config::default()); - let initial_handle = otel.shutdown_handle(); - - // Can use the initial handle - let _sender1 = initial_handle.sender(); - - // Can still get more handles from the instance - let additional_handle = otel.shutdown_handle(); - let _sender2 = additional_handle.sender(); - - // All approaches available - let _result = otel.shutdown().await; - } - } } From 90b4f5c6a59d1c7a815fe2e078b666fddb931762 Mon Sep 17 00:00:00 2001 From: Nick Patilsen Date: Fri, 14 Nov 2025 04:28:30 +0000 Subject: [PATCH 9/9] spelling --- src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 1d3643f..59e232c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -161,10 +161,10 @@ impl Otel { info!("shutting down otel component"); if let Err(metrics_error) = self.meter_provider.force_flush() { - warn!("ecountered error while flushing metrics: {metrics_error:?}"); + warn!("encountered error while flushing metrics: {metrics_error:?}"); } if let Err(metrics_error) = self.meter_provider.shutdown() { - warn!("ecountered error while shutting down meter provider: {metrics_error:?}"); + warn!("encountered error while shutting down meter provider: {metrics_error:?}"); } if let Some(logger_provider) = self.logger_provider.clone() {