Skip to main content

slack_log/
logger.rs

1use std::sync::OnceLock;
2use serde::Serialize;
3use serde_json::{json, Value};
4use reqwest::Client;
5use tokio; // make sure `tokio` is in Cargo.toml
6
7#[derive(Debug)]
8pub enum LogLevel {
9    DEFAULT,
10    SUCCESS,
11    INFO,
12    WARN,
13    ERROR,
14}
15
16impl LogLevel {
17    pub fn color(&self) -> &'static str {
18        match self {
19            LogLevel::DEFAULT => "#B4B4B8",
20            LogLevel::SUCCESS => "#65B741",
21            LogLevel::INFO => "#40A2D8",
22            LogLevel::WARN => "#E3651D",
23            LogLevel::ERROR => "#FF0000",
24        }
25    }
26}
27
28#[derive(Debug, Serialize, Clone)]
29pub struct BlockField {
30    pub title: String,
31    pub value: Value,
32}
33
34impl BlockField {
35    pub fn new<T: Serialize>(title: impl Into<String>, value: T) -> Self {
36        BlockField {
37            title: title.into(),
38            value: serde_json::to_value(value).unwrap_or(Value::Null),
39        }
40    }
41}
42
43#[derive(Debug, Clone)]
44pub struct SlackConfig {
45    pub webhook_url: String,
46    pub debugger: bool,
47}
48
49static CONFIG: OnceLock<SlackConfig> = OnceLock::new();
50
51pub fn slack_log_initialize(config: SlackConfig) {
52    CONFIG.set(config).expect("SlackLogger already initialized");
53}
54
55fn get_config() -> &'static SlackConfig {
56    CONFIG
57        .get()
58        .expect("SlackLogger not initialized. Call `slack_log_initialize()` first.")
59}
60
61fn get_webhook_url() -> Option<String> {
62    let config = get_config();
63    if config.webhook_url.starts_with("https://") {
64        Some(config.webhook_url.clone())
65    } else {
66        None
67    }
68}
69
70fn is_debug() -> bool {
71    get_config().debugger
72}
73
74async fn send_async(payload: Value) {
75    if let Some(url) = get_webhook_url() {
76        let client = Client::new();
77        match client.post(url).json(&payload).send().await {
78            Ok(_) => {
79                if is_debug() {
80                    // println!("✅ Slack log sent.");
81                }
82            }
83            Err(e) => {
84                eprintln!("🚨 Failed to send Slack log: {}", e);
85            }
86        }
87    } else {
88        eprintln!("🚨 Invalid or missing Slack Webhook URL");
89    }
90}
91
92// ---------- Improved Simple Text Logs with Optional Fields ----------
93
94pub fn log<T: Serialize + Send + 'static>(data: T) {
95    spawn_dynamic("", data);
96}
97pub fn log_info<T: Serialize + Send + 'static>(data: T) {
98    spawn_dynamic("ℹ INFO: ", data);
99}
100pub fn log_warn<T: Serialize + Send + 'static>(data: T) {
101    spawn_dynamic("⚠ WARNING: ", data);
102}
103pub fn log_error<T: Serialize + Send + 'static>(data: T) {
104    spawn_dynamic("🚨 ERROR: ", data);
105}
106pub fn log_success<T: Serialize + Send + 'static>(data: T) {
107    spawn_dynamic("✅ SUCCESS: ", data);
108}
109
110fn spawn_dynamic<T: Serialize + Send + 'static>(prefix: &str, data: T) {
111    let prefix = prefix.to_string();
112    tokio::spawn(async move {
113        send_dynamic(&prefix, data).await;
114    });
115}
116
117async fn send_dynamic<T: Serialize>(prefix: &str, data: T) {
118    let value: Value = serde_json::to_value(data).unwrap_or(Value::Null);
119
120    let message = match &value {
121        // Value::String(s) => format!("{prefix}: {s}"),
122        Value::String(s) => {
123            if s.contains('\n') {
124                format!("{prefix}\n```{}```", s)
125            } else {
126                format!("{prefix}{s}")
127            }
128        },
129        Value::Number(n) => format!("{prefix}: {n}"),
130        Value::Bool(b) => format!("{prefix}: {b}"),
131        _ => format!("{prefix}:\n```{}```", serde_json::to_string_pretty(&value).unwrap_or_else(|_| "serialization error".to_string())),
132        // _ => format!("{prefix}:\n{}", serde_json::to_string_pretty(&value).unwrap_or_default()),
133    };
134    let payload = json!({ "text": message });
135
136    send_async(payload).await;
137}
138
139// ---------- Block Logs with Thread Safety ----------
140
141pub fn log_block(label: &str, fields: &[BlockField]) {
142    spawn_block_log(label, fields, LogLevel::DEFAULT);
143}
144
145pub fn log_block_success(label: &str, fields: &[BlockField]) {
146    spawn_block_log(label, fields, LogLevel::SUCCESS);
147}
148pub fn log_block_info(label: &str, fields: &[BlockField]) {
149    spawn_block_log(label, fields, LogLevel::INFO);
150}
151pub fn log_block_warn(label: &str, fields: &[BlockField]) {
152    spawn_block_log(label, fields, LogLevel::WARN);
153}
154pub fn log_block_error(label: &str, fields: &[BlockField]) {
155    spawn_block_log(label, fields, LogLevel::ERROR);
156}
157
158fn spawn_block_log(label: &str, fields: &[BlockField], level: LogLevel) {
159    let label = label.to_string();
160    let fields = fields.to_vec();
161    tokio::spawn(async move {
162        log_block_message(&label, &fields, level).await;
163    });
164}
165
166pub async fn log_block_message(label: &str, fields: &[BlockField], level: LogLevel) {
167    let blocks = vec![
168        json!({ "type": "divider" }),
169        json!({
170            "type": "header",
171            "text": { "type": "plain_text", "text": label, "emoji": true }
172        }),
173        json!({ "type": "divider" }),
174    ];
175
176    let attachment_blocks: Vec<_> = fields
177        .iter()
178        .map(|field| {
179            let raw = serde_json::to_string_pretty(&field.value).unwrap_or_else(|_| "serialization error".to_string());
180
181            let formatted_value = if raw.contains('\n') {
182                format!("```{}```", raw)
183            } else {
184                raw
185            };
186
187            json!({
188                "type": "section",
189                "text": {
190                    "type": "mrkdwn",
191                    "text": format!("*{}:* {}", field.title, formatted_value)
192                }
193            })
194        })
195        .collect();
196
197    let payload = json!({
198        "text": label,
199        "blocks": blocks,
200        "attachments": [{
201            "color": level.color(),
202            "blocks": attachment_blocks
203        }]
204    });
205
206    send_async(payload).await;
207}
208
209// Slack struct with instance methods
210pub struct Slack;
211
212impl Slack {
213    pub fn log<T: Serialize + Send + 'static>(&self, body: T) {
214        crate::logger::log(body);
215    }
216
217    pub fn success<T: Serialize + Send + 'static>(&self, body: T) {
218        crate::logger::log_success(body);
219    }
220
221    pub fn info<T: Serialize + Send + 'static>(&self, body: T) {
222        crate::logger::log_info(body);
223    }
224
225    pub fn warn<T: Serialize + Send + 'static>(&self, body: T) {
226        crate::logger::log_warn(body);
227    }
228
229    pub fn error<T: Serialize + Send + 'static>(&self, body: T) {
230        crate::logger::log_error(body);
231    }
232
233    pub fn block(&self, title: &str, body: &[BlockField]) {
234        crate::logger::log_block(title, body);
235    }
236
237    pub fn success_block(&self, title: &str, body: &[BlockField]) {
238        crate::logger::log_block_success(title, body);
239    }
240
241    pub fn info_block(&self, title: &str, body: &[BlockField]) {
242        crate::logger::log_block_info(title, body);
243    }
244
245    pub fn warn_block(&self, title: &str, body: &[BlockField]) {
246        crate::logger::log_block_warn(title, body);
247    }
248
249    pub fn error_block(&self, title: &str, body: &[BlockField]) {
250        crate::logger::log_block_error(title, body);
251    }
252}
253
254// Exported instance
255pub static slack: Slack = Slack;