1use std::sync::OnceLock;
2use serde::Serialize;
3use serde_json::{json, Value};
4use reqwest::Client;
5use tokio; #[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 }
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
92pub 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) => {
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 };
134 let payload = json!({ "text": message });
135
136 send_async(payload).await;
137}
138
139pub 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
209pub 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
254pub static slack: Slack = Slack;