use anyhow::{Context, Result as AnyhowResult};
use serde::{Deserialize, Serialize};
use serde_json::{Value as JsonValue, json};
#[cfg(feature = "worker-v0-5")]
use worker::{Env, Fetch, Method, Request, RequestInit, console_error};
#[cfg(feature = "worker-v0-4")]
use worker_v4::{Env, Fetch, Method, Request, RequestInit, console_error};
#[derive(Serialize, Deserialize, Debug, Clone, Copy)]
pub enum Severity {
Warn,
Error,
Info,
Debug,
}
impl ToString for Severity {
fn to_string(&self) -> String {
match self {
Severity::Warn => "warn".to_string(),
Severity::Error => "error".to_string(),
Severity::Info => "info".to_string(),
Severity::Debug => "debug".to_string(),
}
}
}
#[derive(Serialize, Deserialize)]
struct Payload {
service: String,
route: String,
severity: Severity,
message: String,
code: u16,
#[serde(skip_serializing_if = "Option::is_none")]
timestamp: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
context: Option<String>,
}
impl Payload {
fn new(
service: impl Into<String>,
route: impl Into<String>,
severity: Severity,
message: impl Into<String>,
code: u16,
context: Option<JsonValue>,
) -> Self {
Payload {
service: service.into(),
route: route.into(),
severity,
message: message.into(),
code,
timestamp: None, context: context
.and_then(|v| serde_json::to_string(&v).ok())
.filter(|s| !s.is_empty() && s != "{}"),
}
}
}
pub async fn save_log(
env: &Env,
url: &str,
message: &str,
code: u16,
data: &JsonValue,
severity: Severity,
) -> AnyhowResult<()> {
let service_name = env
.var("SERVICE_NAME")
.map(|v| v.to_string())
.unwrap_or_else(|_| "unknown".to_string());
let logger_url = env
.var("LOGGER_URL")
.context("LOGGER_URL environment variable not set")?
.to_string();
let logger_auth = env
.var("LOGGER_AUTH")
.context("LOGGER_AUTH environment variable not set")?
.to_string();
let payload = Payload::new(
service_name,
url,
severity,
message,
code,
Some(data.clone()).filter(|v| !v.is_null() && v != &json!({})),
);
let payload = Payload {
timestamp: Some(chrono::Utc::now().to_rfc3339()),
..payload
};
let body = serde_json::to_string(&payload).context("Failed to serialize log payload")?;
let mut headers = worker::Headers::new();
headers
.append("Content-Type", "application/json")
.context("Failed to set Content-Type header")?;
headers
.append("Authorization", &format!("Basic {}", logger_auth))
.context("Failed to set Authorization header")?;
let req = Request::new_with_init(
&format!("{}/log", logger_url),
&RequestInit {
method: Method::Post,
body: Some(worker::wasm_bindgen::JsValue::from_str(&body)),
headers,
..Default::default()
},
)
.context("Failed to create HTTP request")?;
let mut resp = Fetch::Request(req)
.send()
.await
.context("Failed to send log request")?;
if !(200..=299).contains(&resp.status_code()) {
let status = resp.status_code();
let text = resp
.text()
.await
.unwrap_or_else(|_| "No response body".to_string());
console_error!("Failed to send log: status {}, response: {}", status, text);
return Err(anyhow::anyhow!(
"Log request failed with status {}: {}",
status,
text
));
}
Ok(())
}