use std::sync::OnceLock;
use serde::Serialize;
use serde_json::{json, Value};
use reqwest::Client;
use tokio;
#[derive(Debug)]
pub enum LogLevel {
DEFAULT,
SUCCESS,
INFO,
WARN,
ERROR,
}
impl LogLevel {
pub fn color(&self) -> &'static str {
match self {
LogLevel::DEFAULT => "#B4B4B8",
LogLevel::SUCCESS => "#65B741",
LogLevel::INFO => "#40A2D8",
LogLevel::WARN => "#E3651D",
LogLevel::ERROR => "#FF0000",
}
}
}
#[derive(Debug, Serialize, Clone)]
pub struct BlockField {
pub title: String,
pub value: Value,
}
impl BlockField {
pub fn new<T: Serialize>(title: impl Into<String>, value: T) -> Self {
BlockField {
title: title.into(),
value: serde_json::to_value(value).unwrap_or(Value::Null),
}
}
}
#[derive(Debug, Clone)]
pub struct SlackConfig {
pub webhook_url: String,
pub debugger: bool,
}
static CONFIG: OnceLock<SlackConfig> = OnceLock::new();
pub fn slack_log_initialize(config: SlackConfig) {
CONFIG.set(config).expect("SlackLogger already initialized");
}
fn get_config() -> &'static SlackConfig {
CONFIG
.get()
.expect("SlackLogger not initialized. Call `slack_log_initialize()` first.")
}
fn get_webhook_url() -> Option<String> {
let config = get_config();
if config.webhook_url.starts_with("https://") {
Some(config.webhook_url.clone())
} else {
None
}
}
fn is_debug() -> bool {
get_config().debugger
}
async fn send_async(payload: Value) {
if let Some(url) = get_webhook_url() {
let client = Client::new();
match client.post(url).json(&payload).send().await {
Ok(_) => {
if is_debug() {
println!("✅ Slack log sent.");
}
}
Err(e) => {
eprintln!("🚨 Failed to send Slack log: {}", e);
}
}
} else {
eprintln!("🚨 Invalid or missing Slack Webhook URL");
}
}
pub fn log<T: Serialize + Send + 'static>(data: T) {
spawn_dynamic("🔸 LOG", data);
}
pub fn log_info<T: Serialize + Send + 'static>(data: T) {
spawn_dynamic("ℹ INFO", data);
}
pub fn log_warn<T: Serialize + Send + 'static>(data: T) {
spawn_dynamic("⚠ WARNING", data);
}
pub fn log_error<T: Serialize + Send + 'static>(data: T) {
spawn_dynamic("🚨 ERROR", data);
}
pub fn log_success<T: Serialize + Send + 'static>(data: T) {
spawn_dynamic("✅ SUCCESS", data);
}
fn spawn_dynamic<T: Serialize + Send + 'static>(prefix: &str, data: T) {
let prefix = prefix.to_string();
tokio::spawn(async move {
send_dynamic(&prefix, data).await;
});
}
async fn send_dynamic<T: Serialize>(prefix: &str, data: T) {
let value: Value = serde_json::to_value(data).unwrap_or(Value::Null);
let message = match &value {
Value::String(s) => {
if s.contains('\n') {
format!("{prefix}:\n```{}```", s)
} else {
format!("{prefix}: {s}")
}
},
Value::Number(n) => format!("{prefix}: {n}"),
Value::Bool(b) => format!("{prefix}: {b}"),
_ => format!("{prefix}:\n```{}```", serde_json::to_string_pretty(&value).unwrap_or_else(|_| "serialization error".to_string())),
};
let payload = json!({ "text": message });
send_async(payload).await;
}
pub fn log_block(label: &str, fields: &[BlockField]) {
spawn_block_log(label, fields, LogLevel::DEFAULT);
}
pub fn log_block_success(label: &str, fields: &[BlockField]) {
spawn_block_log(label, fields, LogLevel::SUCCESS);
}
pub fn log_block_info(label: &str, fields: &[BlockField]) {
spawn_block_log(label, fields, LogLevel::INFO);
}
pub fn log_block_warn(label: &str, fields: &[BlockField]) {
spawn_block_log(label, fields, LogLevel::WARN);
}
pub fn log_block_error(label: &str, fields: &[BlockField]) {
spawn_block_log(label, fields, LogLevel::ERROR);
}
fn spawn_block_log(label: &str, fields: &[BlockField], level: LogLevel) {
let label = label.to_string();
let fields = fields.to_vec();
tokio::spawn(async move {
log_block_message(&label, &fields, level).await;
});
}
pub async fn log_block_message(label: &str, fields: &[BlockField], level: LogLevel) {
let blocks = vec![
json!({ "type": "divider" }),
json!({
"type": "header",
"text": { "type": "plain_text", "text": label, "emoji": true }
}),
json!({ "type": "divider" }),
];
let attachment_blocks: Vec<_> = fields
.iter()
.map(|field| {
let raw = serde_json::to_string_pretty(&field.value).unwrap_or_else(|_| "serialization error".to_string());
let formatted_value = if raw.contains('\n') {
format!("```{}```", raw)
} else {
raw
};
json!({
"type": "section",
"text": {
"type": "mrkdwn",
"text": format!("*{}:* {}", field.title, formatted_value)
}
})
})
.collect();
let payload = json!({
"text": label,
"blocks": blocks,
"attachments": [{
"color": level.color(),
"blocks": attachment_blocks
}]
});
send_async(payload).await;
}