1use once_cell::sync::OnceCell;
22use reqwest::Client;
23use serde::Serialize;
24use serde_json::json;
25use std::sync::Arc;
26use thiserror::Error;
27use tokio::sync::mpsc::{self, Sender};
28use tokio::time::{self, Duration};
29
30const APP_NAME: &str = env!("CARGO_PKG_VERSION"); static STATE: OnceCell<Arc<SeqState>> = OnceCell::new();
33
34#[derive(Debug, Clone, Copy)]
35pub enum TimeMode {
36 Utc,
37 Local,
38}
39
40impl Default for TimeMode {
41 fn default() -> Self {
42 TimeMode::Utc
43 }
44}
45#[derive(Debug, Clone)]
46pub struct Config {
47 pub application_name: Option<String>, pub endpoint: String, pub api_key: Option<String>, pub queue_capacity: usize, pub flush_interval_ms: u64, pub time_mode: TimeMode, pub enable_panic_hook: bool, }
55
56#[derive(Clone)]
57struct SeqState {
58 tx: Sender<Event>,
59 time_mode: TimeMode, app_name: String,
61}
62
63#[derive(Debug, Error)]
64pub enum InitError {
65 #[error("already initialized")]
66 AlreadyInitialized,
67 #[error("http client error: {0}")]
68 Http(#[from] reqwest::Error),
69}
70
71#[derive(Debug, Error)]
72pub enum LogError {
73 #[error("logger not initialized")]
74 NotInitialized,
75 #[error("queue full, message dropped")]
76 QueueFull,
77}
78
79#[derive(Debug, Serialize)]
80struct Event {
81 #[serde(rename = "@t")]
82 timestamp: String, #[serde(rename = "@mt")]
84 message_template: String,
85 #[serde(rename = "@l")]
86 level: String,
87 #[serde(rename = "Application")]
88 application: String,
89 #[serde(flatten)]
90 properties: serde_json::Value,
91}
92
93fn now_rfc3339(mode: TimeMode) -> String {
94 use ::time::format_description::well_known::Rfc3339;
95
96 match mode {
97 TimeMode::Utc => ::time::OffsetDateTime::now_utc()
98 .format(&Rfc3339)
99 .unwrap_or_else(|_| "1970-01-01T00:00:00Z".to_string()),
100 TimeMode::Local => {
101 let dt = ::time::OffsetDateTime::now_local()
102 .unwrap_or_else(|_| ::time::OffsetDateTime::now_utc());
103 dt.format(&Rfc3339)
104 .unwrap_or_else(|_| "1970-01-01T00:00:00Z".to_string())
105 }
106 }
107}
108
109pub mod seq {
111
112 use super::*;
113
114 pub async fn init(cfg: Config) -> Result<(), InitError> {
116 if STATE.get().is_some() {
117 return Err(InitError::AlreadyInitialized);
118 }
119 if cfg.enable_panic_hook {
120 install_panic_hook(cfg.clone());
122 }
123
124 let (tx, mut rx) = mpsc::channel::<Event>(cfg.queue_capacity);
125 let state = Arc::new(SeqState {
126 tx,
127 time_mode: cfg.time_mode,
128 app_name: cfg
129 .application_name
130 .clone()
131 .unwrap_or_else(|| APP_NAME.to_string()),
132 });
133 let endpoint = cfg.endpoint.trim_end_matches('/').to_string();
134 let api_key = cfg.api_key.clone();
135 let client = Client::builder().build()?;
136 let flush_every = Duration::from_millis(cfg.flush_interval_ms);
137
138 tokio::spawn(async move {
140 let mut buf: Vec<Event> = Vec::with_capacity(1024);
141 let mut ticker = time::interval(flush_every);
142
143 loop {
144 tokio::select! {
145 maybe_ev = rx.recv() => {
146 match maybe_ev {
147 Some(ev) => { buf.push(ev); }
148 None => { let _ = try_flush(&client, &endpoint, api_key.as_deref(), &mut buf).await;
150 break;
151 }
152 }
153 }
154 _ = ticker.tick() => {
155 let _ = try_flush(&client, &endpoint, api_key.as_deref(), &mut buf).await;
156 }
157 }
158 }
159 });
160
161 STATE.set(state).ok();
162 Ok(())
163 }
164
165 pub async fn info(message: &str, props: serde_json::Value) {
167 let _ = enqueue("Information", message, props).await;
168 }
169
170 pub async fn warn(message: &str, props: serde_json::Value) {
172 let _ = enqueue("Warning", message, props).await;
173 }
174
175 pub async fn error(message: &str, props: serde_json::Value) {
177 let _ = enqueue("Error", message, props).await;
178 }
179
180 pub async fn info_msg(message: &str) {
182 info(message, json!({})).await
183 }
184 pub async fn warn_msg(message: &str) {
185 warn(message, json!({})).await
186 }
187 pub async fn error_msg(message: &str) {
188 error(message, json!({})).await
189 }
190
191 async fn enqueue(level: &str, message: &str, props: serde_json::Value) -> Result<(), LogError> {
192 let state = STATE.get().ok_or(LogError::NotInitialized)?.clone();
193 let now = now_rfc3339(state.time_mode);
195
196 let mut properties = props;
197 if !properties.get("App").is_some() {
199 if let Some(obj) = properties.as_object_mut() {
200 obj.insert("App".into(), json!(state.app_name));
201 }
202 }
203
204 let ev = Event {
205 timestamp: now,
206 message_template: message.to_string(),
207 level: level.to_string(),
208 application: state.app_name.to_string(),
209 properties,
210 };
211
212 state.tx.try_send(ev).map_err(|_| LogError::QueueFull)?;
213 Ok(())
214 }
215
216 async fn try_flush(
217 client: &Client,
218 endpoint: &str,
219 api_key: Option<&str>,
220 buf: &mut Vec<Event>,
221 ) -> Result<(), reqwest::Error> {
222 if buf.is_empty() {
223 return Ok(());
224 }
225
226 let clef_lines = buf
228 .iter()
229 .map(|e| serde_json::to_string(e).unwrap_or_default())
230 .collect::<Vec<_>>()
231 .join("\n");
232
233 let url = format!("{}/api/events/raw?clef", endpoint);
234 let mut req = client
235 .post(url)
236 .body(clef_lines)
237 .header("Content-Type", "application/vnd.serilog.clef");
238
239 if let Some(key) = api_key {
240 req = req.header("X-Seq-ApiKey", key);
241 }
242
243 let res = req.send().await?;
244 if res.status().is_success() {
245 buf.clear(); } else {
247 eprintln!(
248 "SEQ response {}: {}",
249 res.status(),
250 res.text().await.unwrap_or_default()
251 );
252 }
253
254 Ok(())
255 }
256}
257use std::panic;
258
259fn install_panic_hook(config: Config) {
263 #[derive(Clone)]
265 struct PanicConfig {
266 endpoint: String,
267 api_key: Option<String>,
268 }
269 let panic_cfg = PanicConfig {
270 endpoint: config.endpoint.clone(),
271 api_key: config.api_key.clone(),
272 };
273
274 panic::set_hook(Box::new(move |panic_info| {
275 let location = panic_info
276 .location()
277 .map(|l| format!("{}:{}", l.file(), l.line()))
278 .unwrap_or_else(|| "unknown".into());
279
280 let msg = if let Some(s) = panic_info.payload().downcast_ref::<&str>() {
281 s.to_string()
282 } else if let Some(s) = panic_info.payload().downcast_ref::<String>() {
283 s.clone()
284 } else {
285 "Unknown panic payload".into()
286 };
287
288 eprintln!("💥 Panic at {location}: {msg}");
289
290 if let Some(state) = STATE.get() {
291 let ev = Event {
293 timestamp: now_rfc3339(state.time_mode),
294 message_template: "Panic occurred".to_string(),
295 level: "Error".to_string(),
296 application: state.app_name.to_string(),
297 properties: serde_json::json!({
298 "location": location,
299 "message": msg,
300 "thread": std::thread::current().name(),
301 }),
302 };
303
304 send_event_sync(&panic_cfg.endpoint, panic_cfg.api_key.as_deref(), &ev);
305 }
306 }));
307}
308
309fn send_event_sync(endpoint: &str, api_key: Option<&str>, event: &Event) {
310 if let Ok(line) = serde_json::to_string(event) {
311 if let Ok(client) = reqwest::blocking::Client::builder()
312 .timeout(std::time::Duration::from_millis(800))
313 .build()
314 {
315 let mut req = client
316 .post(format!("{}/api/events/raw?clef", endpoint))
317 .header("Content-Type", "application/vnd.serilog.clef")
318 .body(line);
319 if let Some(key) = api_key {
320 req = req.header("X-Seq-ApiKey", key);
321 }
322 let _ = req.send();
323 }
324 }
325}