mirror of
https://github.com/storytold/artcraft.git
synced 2026-10-26 19:58:46 -04:00
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
707711b357 | ||
|
|
207b1bc922 | ||
|
|
0c9a778cda |
@@ -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(¬ification).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)?;
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,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."),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user