#![warn(missing_docs)]
pub mod health;
#[cfg(feature = "metrics")]
pub mod metrics;
#[cfg(feature = "otlp")]
pub mod tracing;
#[cfg(feature = "profiling")]
pub mod profiling;
#[cfg(feature = "json-log")]
pub mod logging;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use wae_types::WaeError;
#[cfg(feature = "json-log")]
use ::tracing::Level;
#[cfg(any(feature = "metrics", feature = "health", feature = "profiling"))]
use std::sync::Arc;
pub type ObservabilityResult<T> = Result<T, WaeError>;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
pub enum HealthStatus {
#[default]
Passing,
Warning,
Critical,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HealthCheck {
pub name: String,
pub status: HealthStatus,
pub details: Option<String>,
pub timestamp: i64,
}
impl HealthCheck {
pub fn new(name: String, status: HealthStatus) -> Self {
Self { name, status, details: None, timestamp: chrono::Utc::now().timestamp() }
}
pub fn with_details(mut self, details: String) -> Self {
self.details = Some(details);
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HealthReport {
pub overall: HealthStatus,
pub checks: Vec<HealthCheck>,
}
impl HealthReport {
pub fn new(checks: Vec<HealthCheck>) -> Self {
let overall = checks.iter().fold(HealthStatus::Passing, |acc, check| match (acc, check.status) {
(HealthStatus::Critical, _) | (_, HealthStatus::Critical) => HealthStatus::Critical,
(HealthStatus::Warning, _) | (_, HealthStatus::Warning) => HealthStatus::Warning,
_ => HealthStatus::Passing,
});
Self { overall, checks }
}
}
#[async_trait::async_trait]
pub trait HealthChecker: Send + Sync {
async fn check(&self) -> HealthCheck;
}
#[cfg(feature = "json-log")]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum LogOutput {
#[default]
Stdout,
Stderr,
Test,
}
#[cfg(feature = "json-log")]
#[derive(Debug, Clone)]
pub struct JsonLogConfig {
pub level: Level,
pub output: LogOutput,
pub include_target: bool,
pub include_timestamp: bool,
}
#[cfg(feature = "json-log")]
impl Default for JsonLogConfig {
fn default() -> Self {
Self { level: Level::INFO, output: LogOutput::Stdout, include_target: true, include_timestamp: true }
}
}
#[cfg(feature = "otlp")]
#[derive(Debug, Clone)]
pub struct OtlpConfig {
pub endpoint: String,
pub service_name: String,
pub service_version: String,
pub sample_ratio: f64,
pub enabled: bool,
}
#[cfg(feature = "otlp")]
impl Default for OtlpConfig {
fn default() -> Self {
Self {
endpoint: "http://localhost:4317".to_string(),
service_name: "wae-service".to_string(),
service_version: "0.1.0".to_string(),
sample_ratio: 1.0,
enabled: false,
}
}
}
#[derive(Debug, Clone)]
pub struct ObservabilityConfig {
pub service_name: String,
pub service_version: String,
pub enable_health: bool,
pub enable_metrics: bool,
pub enable_json_log: bool,
pub enable_otlp: bool,
pub enable_profiling: bool,
#[cfg(feature = "json-log")]
pub json_log_config: JsonLogConfig,
#[cfg(feature = "otlp")]
pub otlp_config: OtlpConfig,
#[cfg(feature = "profiling")]
pub profiling_config: profiling::ConsoleConfig,
}
impl Default for ObservabilityConfig {
fn default() -> Self {
Self {
service_name: "wae-service".to_string(),
service_version: "0.1.0".to_string(),
enable_health: true,
enable_metrics: false,
enable_json_log: false,
enable_otlp: false,
enable_profiling: false,
#[cfg(feature = "json-log")]
json_log_config: JsonLogConfig::default(),
#[cfg(feature = "otlp")]
otlp_config: OtlpConfig::default(),
#[cfg(feature = "profiling")]
profiling_config: profiling::ConsoleConfig::default(),
}
}
}
impl ObservabilityConfig {
pub fn new(service_name: String, service_version: String) -> Self {
Self { service_name, service_version, ..Default::default() }
}
pub fn with_health(mut self) -> Self {
self.enable_health = true;
self
}
#[cfg(feature = "metrics")]
pub fn with_metrics(mut self) -> Self {
self.enable_metrics = true;
self
}
#[cfg(feature = "json-log")]
pub fn with_json_log(mut self) -> Self {
self.enable_json_log = true;
self
}
#[cfg(feature = "otlp")]
pub fn with_otlp(mut self) -> Self {
self.enable_otlp = true;
self
}
#[cfg(feature = "profiling")]
pub fn with_profiling(mut self) -> Self {
self.enable_profiling = true;
self
}
#[cfg(feature = "json-log")]
pub fn with_json_log_config(mut self, config: JsonLogConfig) -> Self {
self.json_log_config = config;
self
}
#[cfg(feature = "otlp")]
pub fn with_otlp_config(mut self, config: OtlpConfig) -> Self {
self.otlp_config = config;
self
}
#[cfg(feature = "profiling")]
pub fn with_profiling_config(mut self, config: profiling::ConsoleConfig) -> Self {
self.profiling_config = config;
self
}
}
pub struct ObservabilityService {
config: ObservabilityConfig,
health_checkers: Vec<Box<dyn HealthChecker>>,
#[cfg(feature = "health")]
health_registry: Option<Arc<health::HealthRegistry>>,
#[cfg(feature = "metrics")]
metrics_registry: Option<Arc<metrics::MetricsRegistry>>,
}
impl ObservabilityService {
pub fn new(config: ObservabilityConfig) -> Self {
Self {
config,
health_checkers: Vec::new(),
#[cfg(feature = "health")]
health_registry: None,
#[cfg(feature = "metrics")]
metrics_registry: None,
}
}
pub fn add_health_checker(&mut self, checker: Box<dyn HealthChecker>) {
self.health_checkers.push(checker);
}
pub async fn health_check(&self) -> ObservabilityResult<HealthReport> {
let mut checks = Vec::with_capacity(self.health_checkers.len());
for checker in &self.health_checkers {
checks.push(checker.check().await);
}
Ok(HealthReport::new(checks))
}
pub fn config(&self) -> &ObservabilityConfig {
&self.config
}
#[cfg(feature = "health")]
pub fn health_registry(&self) -> Option<&Arc<health::HealthRegistry>> {
self.health_registry.as_ref()
}
#[cfg(feature = "metrics")]
pub fn metrics_registry(&self) -> Option<&Arc<metrics::MetricsRegistry>> {
self.metrics_registry.as_ref()
}
}
pub fn init_observability(config: ObservabilityConfig) -> ObservabilityResult<ObservabilityService> {
#[cfg(feature = "json-log")]
let _ = config.enable_json_log;
#[cfg(feature = "otlp")]
let _ = config.enable_otlp;
#[cfg(feature = "profiling")]
if config.enable_profiling {
let console = profiling::TokioConsole::new();
console.init(&config.profiling_config);
}
#[allow(unused_mut)]
let mut service = ObservabilityService::new(config);
#[cfg(feature = "health")]
{
let registry = Arc::new(health::HealthRegistry::new());
service.health_registry = Some(registry);
}
#[cfg(feature = "metrics")]
if service.config.enable_metrics {
let registry = Arc::new(metrics::MetricsRegistry::new());
service.metrics_registry = Some(registry);
}
Ok(service)
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MetricLabels {
labels: HashMap<String, String>,
}
impl MetricLabels {
pub fn new() -> Self {
Self { labels: HashMap::new() }
}
pub fn insert(&mut self, key: String, value: String) {
self.labels.insert(key, value);
}
pub fn get(&self, key: &str) -> Option<&String> {
self.labels.get(key)
}
}
impl Default for MetricLabels {
fn default() -> Self {
Self::new()
}
}
pub struct HttpHealthChecker {
name: String,
url: String,
}
impl HttpHealthChecker {
pub fn new(name: String, url: String) -> Self {
Self { name, url }
}
}
#[async_trait::async_trait]
impl HealthChecker for HttpHealthChecker {
async fn check(&self) -> HealthCheck {
HealthCheck::new(self.name.clone(), HealthStatus::Passing).with_details(format!("HTTP check for {}", self.url))
}
}