Compare commits

...
Author SHA1 Message Date
Brandon Thomas 707711b357 cleanup 2026-03-25 19:27:31 -04:00
Brandon Thomas 207b1bc922 cleanup 2026-03-25 19:20:14 -04:00
Brandon Thomas 0c9a778cda no-op builder 2026-03-25 19:16:29 -04:00
8 changed files with 84 additions and 99 deletions
+19 -7
View File
@@ -1,7 +1,7 @@
use std::sync::Arc;
use std::sync::atomic::AtomicBool;
use std::sync::Arc;
use log::{info, warn};
use log::{debug, info, warn};
use crate::client::pager_client::{PageSentResult, PagerClient};
use crate::error::pager_error::PagerError;
@@ -44,31 +44,43 @@ impl Pager {
}
/// Send a page immediately, blocking until the API responds.
/// Returns `Ok(None)` if the pager is configured as NoOp.
pub async fn send_page_immediately(
&self,
notification: NotificationDetails,
) -> Result<PageSentResult, PagerError> {
) -> Result<Option<PageSentResult>, PagerError> {
self.client.send_page(&notification).await
}
/// Send a page immediately, blocking until the API responds.
/// Send a page immediately. Logs errors but never fails.
/// Returns `None` if the pager is NoOp or if sending failed.
pub async fn send_page_immediately_infallible(
&self,
notification: NotificationDetails,
) {
if let Err(err) = self.send_page_immediately(notification).await {
warn!("Failure sending page: {:?}", err);
) -> Option<PageSentResult> {
match self.send_page_immediately(notification).await {
Ok(result) => result,
Err(err) => {
warn!("Failure sending page: {:?}", err);
None
}
}
}
/// Enqueue a page to be sent by the background worker thread.
///
/// Returns `Ok(())` immediately if the pager is NoOp.
/// Returns an error if the worker is not configured (use `build_with_worker()`).
/// If the queue is full, the oldest item is dropped.
pub fn enqueue_page(
&self,
notification: NotificationDetails,
) -> Result<(), PagerError> {
if self.client.is_noop() {
debug!("Pager no-op: would have enqueued page: {}", notification.summary);
return Ok(());
}
let queue = self.queue.as_ref()
.ok_or(PagerSystemError::QueueNotConfigured)?;
+22 -23
View File
@@ -1,11 +1,10 @@
use std::sync::Arc;
use log::warn;
use rootly_client::creds::rootly_api_key::RootlyApiKey;
use crate::client::pager::Pager;
use crate::client::pager_client::{PagerClient, PagerClientConfig};
use crate::error::pager_builder_error::PagerBuilderError;
use crate::error::pager_error::PagerError;
use crate::worker::pager_worker_message_queue::{new_shared_queue, new_shared_queue_with_capacity, PagerWorkerMessageQueue};
use crate::worker::pager_worker::PagerWorker;
@@ -65,30 +64,25 @@ impl PagerBuilder {
/// Build a `Pager` without a background worker.
///
/// Only `send_page_immediately()` will be available.
/// Calling `enqueue_page()` will return `PagerServiceError::WorkerNotAvailable`.
pub fn build(self) -> Result<Pager, PagerError> {
let client = self.make_client()?;
Ok(Pager::new(client))
/// If no backend was configured, a NoOp pager is returned.
pub fn build(self) -> Pager {
let client = self.make_client();
Pager::new(client)
}
/// Build a `Pager` with a background worker thread.
///
/// Returns both the `Pager` (for enqueuing) and the `PagerWorker` (to spawn).
/// If no backend was configured, a NoOp pager is returned (the worker will idle).
///
/// The caller is responsible for running the worker on a dedicated thread:
/// ```ignore
/// let (pager, worker) = PagerBuilder::new()
/// .application_name("my-service".to_string())
/// .rootly(api_key)
/// .build_with_worker()?;
///
/// std::thread::spawn(move || {
/// let rt = tokio::runtime::Runtime::new().unwrap();
/// rt.block_on(worker.run());
/// });
/// .build_with_worker();
/// ```
pub fn build_with_worker(self) -> Result<(Pager, PagerWorker), PagerError> {
let client = self.make_client()?;
pub fn build_with_worker(self) -> (Pager, PagerWorker) {
let client = self.make_client();
let queue: Arc<PagerWorkerMessageQueue> = match self.queue_capacity {
Some(capacity) => new_shared_queue_with_capacity(capacity),
@@ -99,14 +93,19 @@ impl PagerBuilder {
let shutdown = worker.shutdown_handle();
let pager = Pager::with_queue(client, queue, shutdown);
Ok((pager, worker))
(pager, worker)
}
fn make_client(&self) -> Result<PagerClient, PagerError> {
let client_config = self.client_config.clone()
.ok_or(PagerBuilderError::NoBackendConfigured)?;
fn make_client(&self) -> PagerClient {
let client_config = match self.client_config.clone() {
Some(config) => config,
None => {
warn!("No pager backend configured. Using NoOp pager.");
PagerClientConfig::NoOp
}
};
Ok(PagerClient::new(client_config, self.application_name.clone(), self.environment.clone()))
PagerClient::new(client_config, self.application_name.clone(), self.environment.clone())
}
}
@@ -146,12 +145,12 @@ impl RootlyConfigBuilder {
}
/// Shortcut: finish Rootly config and build without a worker.
pub fn build(self) -> Result<Pager, PagerError> {
pub fn build(self) -> Pager {
self.done().build()
}
/// Shortcut: finish Rootly config and build with a worker.
pub fn build_with_worker(self) -> Result<(Pager, PagerWorker), PagerError> {
pub fn build_with_worker(self) -> (Pager, PagerWorker) {
self.done().build_with_worker()
}
}
+25 -10
View File
@@ -1,4 +1,4 @@
use log::{error, info};
use log::{debug, warn};
use rootly_client::creds::rootly_api_key::RootlyApiKey;
use rootly_client::requests::create_alert::create_alert::{
@@ -25,6 +25,9 @@ pub struct PagerClient {
/// Configuration for the pager client backend.
#[derive(Clone)]
pub enum PagerClientConfig {
/// No-op backend. All calls are silently ignored.
NoOp,
/// Use Rootly as the paging backend.
Rootly {
api_key: RootlyApiKey,
@@ -44,7 +47,7 @@ pub enum PagerClientConfig {
#[derive(Debug, Clone)]
pub struct PageSentResult {
/// A backend-specific identifier for the sent notification.
pub id: String,
pub id: Option<String>,
/// A short human-readable ID (if the backend provides one).
pub short_id: Option<String>,
@@ -59,10 +62,20 @@ impl PagerClient {
Self { client_config, application_name, environment }
}
/// Send a page immediately.
pub async fn send_page(&self, notification: &NotificationDetails) -> Result<PageSentResult, PagerError> {
pub fn is_noop(&self) -> bool {
matches!(self.client_config, PagerClientConfig::NoOp)
}
/// Send a page immediately. Returns `Ok(None)` for NoOp.
pub async fn send_page(&self, notification: &NotificationDetails) -> Result<Option<PageSentResult>, PagerError> {
match &self.client_config {
PagerClientConfig::Rootly { .. } => self.send_page_via_rootly(notification).await,
PagerClientConfig::NoOp => {
debug!("Pager no-op: would have sent page: {}", notification.summary);
Ok(None)
}
PagerClientConfig::Rootly { .. } => {
self.send_page_via_rootly(notification).await.map(Some)
}
}
}
@@ -75,13 +88,15 @@ impl PagerClient {
alert_urgency_id,
notification_target_type,
notification_target_id,
} = &self.client_config;
} = &self.client_config else {
return Ok(PageSentResult { id: None, short_id: None });
};
let source = self.application_name
.clone()
.unwrap_or_else(|| "unknown".to_string());
info!("Sending page via Rootly (source={}): {}", source, notification.summary);
debug!("Sending page via Rootly (source={}): {}", source, notification.summary);
let mut labels: Vec<(String, String)> = Vec::new();
@@ -111,14 +126,14 @@ impl PagerClient {
match result {
Ok(success) => {
info!("Page sent successfully via Rootly: id={}, short_id={:?}", success.id, success.short_id);
debug!("Page sent successfully via Rootly: id={}, short_id={:?}", success.id, success.short_id);
Ok(PageSentResult {
id: success.id,
id: Some(success.id),
short_id: success.short_id,
})
}
Err(err) => {
error!("Failed to send page via Rootly: {}", err);
warn!("Failed to send page via Rootly: {}", err);
Err(PagerError::Service(PagerServiceError::RootlyError(err)))
}
}
-1
View File
@@ -1,4 +1,3 @@
pub mod pager_builder_error;
pub mod pager_error;
pub mod pager_service_error;
pub mod pager_system_error;
@@ -1,19 +0,0 @@
use std::error::Error;
use std::fmt::{Display, Formatter};
/// Errors from building/configuring a Pager instance.
#[derive(Debug)]
pub enum PagerBuilderError {
/// The pager was not configured with a backend.
NoBackendConfigured,
}
impl Error for PagerBuilderError {}
impl Display for PagerBuilderError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Self::NoBackendConfigured => write!(f, "No pager backend configured. Call .rootly() or .client_config() on PagerBuilder."),
}
}
}
-11
View File
@@ -1,16 +1,12 @@
use std::error::Error;
use std::fmt::{Display, Formatter};
use crate::error::pager_builder_error::PagerBuilderError;
use crate::error::pager_service_error::PagerServiceError;
use crate::error::pager_system_error::PagerSystemError;
/// Top-level error type for the pager library.
#[derive(Debug)]
pub enum PagerError {
/// An error during Pager construction/configuration.
Builder(PagerBuilderError),
/// A third-party paging service (Rootly, etc.) returned an error.
Service(PagerServiceError),
@@ -23,19 +19,12 @@ impl Error for PagerError {}
impl Display for PagerError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Self::Builder(err) => write!(f, "PagerBuilderError: {}", err),
Self::Service(err) => write!(f, "PagerServiceError: {}", err),
Self::System(err) => write!(f, "PagerSystemError: {}", err),
}
}
}
impl From<PagerBuilderError> for PagerError {
fn from(err: PagerBuilderError) -> Self {
Self::Builder(err)
}
}
impl From<PagerServiceError> for PagerError {
fn from(err: PagerServiceError) -> Self {
Self::Service(err)
+9 -6
View File
@@ -1,8 +1,8 @@
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::time::Duration;
use log::{error, info, warn};
use log::{debug, error, info, warn};
use crate::client::pager_client::PagerClient;
use crate::worker::pager_worker_message_queue::PagerWorkerMessageQueue;
@@ -67,7 +67,7 @@ impl PagerWorker {
continue;
}
info!("Pager worker processing {} queued notification(s).", items.len());
debug!("Pager worker processing {} queued notification(s).", items.len());
for notification in &items {
if self.shutdown.load(Ordering::Relaxed) {
@@ -76,12 +76,15 @@ impl PagerWorker {
}
match self.client.send_page(notification).await {
Ok(success) => {
info!(
"Pager worker sent page: id={}, summary={}",
Ok(Some(success)) => {
debug!(
"Pager worker sent page: id={:?}, summary={}",
success.id, notification.summary
);
}
Ok(None) => {
// NoOp — already logged by the client.
}
Err(err) => {
error!(
"Pager worker failed to send page for '{}': {}",
@@ -13,22 +13,24 @@ pub fn build_pager(server_environment: server_environment::ServerEnvironment) ->
let maybe_api_key = easyenv::get_env_string_optional("ROOTLY_API_KEY");
let builder = PagerBuilder::new()
.application_name("storyteller-web".to_string())
.environment(environment.to_string());
match maybe_api_key {
Some(api_key) => {
info!("Rootly API key found. Configuring pager with Rootly backend.");
build_rootly_pager(environment, api_key)
build_rootly_pager(builder, api_key)
}
None => {
warn!("ROOTLY_API_KEY not set. Pager will not send real pages.");
build_fallback_pager(environment)
builder.build_with_worker()
}
}
}
fn build_rootly_pager(environment: &str, api_key: String) -> (Pager, PagerWorker) {
let mut rootly_builder = PagerBuilder::new()
.application_name("storyteller-web".to_string())
.environment(environment.to_string())
fn build_rootly_pager(mut builder: PagerBuilder, api_key: String) -> (Pager, PagerWorker) {
let mut rootly_builder = builder
.rootly(RootlyApiKey::new(api_key));
if let Some(urgency_id) = easyenv::get_env_string_optional("ROOTLY_ALERT_URGENCY_ID") {
@@ -42,20 +44,5 @@ fn build_rootly_pager(environment: &str, api_key: String) -> (Pager, PagerWorker
rootly_builder = rootly_builder.notification_target(t_type, t_id);
}
match rootly_builder.build_with_worker() {
Ok((pager, worker)) => (pager, worker),
Err(err) => {
warn!("Failed to build pager with Rootly backend: {}. Pages will not be sent.", err);
build_fallback_pager(environment)
}
}
}
fn build_fallback_pager(environment: &str) -> (Pager, PagerWorker) {
PagerBuilder::new()
.application_name("storyteller-web".to_string())
.environment(environment.to_string())
.rootly(RootlyApiKey::new(String::new()))
.build_with_worker()
.expect("fallback pager build should not fail")
rootly_builder.build_with_worker()
}