use once_cell::sync::OnceCell;
use reqwest::Client;
use serde::Serialize;
use serde_json::json;
use std::sync::Arc;
use thiserror::Error;
use tokio::sync::mpsc::{self, Sender};
use tokio::time::{self, Duration};
const APP_NAME: &str = env!("CARGO_PKG_VERSION");
static STATE: OnceCell<Arc<SeqState>> = OnceCell::new();
#[derive(Debug, Clone, Copy)]
pub enum TimeMode {
Utc,
Local,
}
impl Default for TimeMode {
fn default() -> Self {
TimeMode::Utc
}
}
#[derive(Debug, Clone)]
pub struct Config {
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, }
#[derive(Clone)]
struct SeqState {
tx: Sender<Event>,
time_mode: TimeMode, app_name: String,
}
#[derive(Debug, Error)]
pub enum InitError {
#[error("already initialized")]
AlreadyInitialized,
#[error("http client error: {0}")]
Http(#[from] reqwest::Error),
}
#[derive(Debug, Error)]
pub enum LogError {
#[error("logger not initialized")]
NotInitialized,
#[error("queue full, message dropped")]
QueueFull,
}
#[derive(Debug, Serialize)]
struct Event {
#[serde(rename = "@t")]
timestamp: String, #[serde(rename = "@mt")]
message_template: String,
#[serde(rename = "@l")]
level: String,
#[serde(rename = "Application")]
application: String,
#[serde(flatten)]
properties: serde_json::Value,
}
fn now_rfc3339(mode: TimeMode) -> String {
use ::time::format_description::well_known::Rfc3339;
match mode {
TimeMode::Utc => ::time::OffsetDateTime::now_utc()
.format(&Rfc3339)
.unwrap_or_else(|_| "1970-01-01T00:00:00Z".to_string()),
TimeMode::Local => {
let dt = ::time::OffsetDateTime::now_local()
.unwrap_or_else(|_| ::time::OffsetDateTime::now_utc());
dt.format(&Rfc3339)
.unwrap_or_else(|_| "1970-01-01T00:00:00Z".to_string())
}
}
}
pub mod seq {
use super::*;
pub async fn init(cfg: Config) -> Result<(), InitError> {
if STATE.get().is_some() {
return Err(InitError::AlreadyInitialized);
}
if cfg.enable_panic_hook {
install_panic_hook(cfg.clone());
}
let (tx, mut rx) = mpsc::channel::<Event>(cfg.queue_capacity);
let state = Arc::new(SeqState {
tx,
time_mode: cfg.time_mode,
app_name: cfg
.application_name
.clone()
.unwrap_or_else(|| APP_NAME.to_string()),
});
let endpoint = cfg.endpoint.trim_end_matches('/').to_string();
let api_key = cfg.api_key.clone();
let client = Client::builder().build()?;
let flush_every = Duration::from_millis(cfg.flush_interval_ms);
tokio::spawn(async move {
let mut buf: Vec<Event> = Vec::with_capacity(1024);
let mut ticker = time::interval(flush_every);
loop {
tokio::select! {
maybe_ev = rx.recv() => {
match maybe_ev {
Some(ev) => { buf.push(ev); }
None => { let _ = try_flush(&client, &endpoint, api_key.as_deref(), &mut buf).await;
break;
}
}
}
_ = ticker.tick() => {
let _ = try_flush(&client, &endpoint, api_key.as_deref(), &mut buf).await;
}
}
}
});
STATE.set(state).ok();
Ok(())
}
pub async fn info(message: &str, props: serde_json::Value) {
let _ = enqueue("Information", message, props).await;
}
pub async fn warn(message: &str, props: serde_json::Value) {
let _ = enqueue("Warning", message, props).await;
}
pub async fn error(message: &str, props: serde_json::Value) {
let _ = enqueue("Error", message, props).await;
}
pub async fn info_msg(message: &str) {
info(message, json!({})).await
}
pub async fn warn_msg(message: &str) {
warn(message, json!({})).await
}
pub async fn error_msg(message: &str) {
error(message, json!({})).await
}
async fn enqueue(level: &str, message: &str, props: serde_json::Value) -> Result<(), LogError> {
let state = STATE.get().ok_or(LogError::NotInitialized)?.clone();
let now = now_rfc3339(state.time_mode);
let mut properties = props;
if !properties.get("App").is_some() {
if let Some(obj) = properties.as_object_mut() {
obj.insert("App".into(), json!(state.app_name));
}
}
let ev = Event {
timestamp: now,
message_template: message.to_string(),
level: level.to_string(),
application: state.app_name.to_string(),
properties,
};
state.tx.try_send(ev).map_err(|_| LogError::QueueFull)?;
Ok(())
}
async fn try_flush(
client: &Client,
endpoint: &str,
api_key: Option<&str>,
buf: &mut Vec<Event>,
) -> Result<(), reqwest::Error> {
if buf.is_empty() {
return Ok(());
}
let clef_lines = buf
.iter()
.map(|e| serde_json::to_string(e).unwrap_or_default())
.collect::<Vec<_>>()
.join("\n");
let url = format!("{}/api/events/raw?clef", endpoint);
let mut req = client
.post(url)
.body(clef_lines)
.header("Content-Type", "application/vnd.serilog.clef");
if let Some(key) = api_key {
req = req.header("X-Seq-ApiKey", key);
}
let res = req.send().await?;
if res.status().is_success() {
buf.clear(); } else {
eprintln!(
"SEQ response {}: {}",
res.status(),
res.text().await.unwrap_or_default()
);
}
Ok(())
}
}
use std::panic;
fn install_panic_hook(config: Config) {
#[derive(Clone)]
struct PanicConfig {
endpoint: String,
api_key: Option<String>,
}
let panic_cfg = PanicConfig {
endpoint: config.endpoint.clone(),
api_key: config.api_key.clone(),
};
panic::set_hook(Box::new(move |panic_info| {
let location = panic_info
.location()
.map(|l| format!("{}:{}", l.file(), l.line()))
.unwrap_or_else(|| "unknown".into());
let msg = if let Some(s) = panic_info.payload().downcast_ref::<&str>() {
s.to_string()
} else if let Some(s) = panic_info.payload().downcast_ref::<String>() {
s.clone()
} else {
"Unknown panic payload".into()
};
eprintln!("💥 Panic at {location}: {msg}");
if let Some(state) = STATE.get() {
let ev = Event {
timestamp: now_rfc3339(state.time_mode),
message_template: "Panic occurred".to_string(),
level: "Error".to_string(),
application: state.app_name.to_string(),
properties: serde_json::json!({
"location": location,
"message": msg,
"thread": std::thread::current().name(),
}),
};
send_event_sync(&panic_cfg.endpoint, panic_cfg.api_key.as_deref(), &ev);
}
}));
}
fn send_event_sync(endpoint: &str, api_key: Option<&str>, event: &Event) {
if let Ok(line) = serde_json::to_string(event) {
if let Ok(client) = reqwest::blocking::Client::builder()
.timeout(std::time::Duration::from_millis(800))
.build()
{
let mut req = client
.post(format!("{}/api/events/raw?clef", endpoint))
.header("Content-Type", "application/vnd.serilog.clef")
.body(line);
if let Some(key) = api_key {
req = req.header("X-Seq-ApiKey", key);
}
let _ = req.send();
}
}
}