1use anyhow::{Context, Result as AnyhowResult};
2use serde::{Deserialize, Serialize};
3use serde_json::{Value as JsonValue, json};
4#[cfg(feature = "worker-v0-5")]
5use worker::{Env, Fetch, Method, Request, RequestInit, console_error};
6#[cfg(feature = "worker-v0-4")]
7use worker_v4::{Env, Fetch, Method, Request, RequestInit, console_error};
8
9#[derive(Serialize, Deserialize, Debug, Clone, Copy)]
11pub enum Severity {
12 Warn,
13 Error,
14 Info,
15 Debug,
16}
17
18impl ToString for Severity {
19 fn to_string(&self) -> String {
20 match self {
21 Severity::Warn => "warn".to_string(),
22 Severity::Error => "error".to_string(),
23 Severity::Info => "info".to_string(),
24 Severity::Debug => "debug".to_string(),
25 }
26 }
27}
28
29#[derive(Serialize, Deserialize)]
31struct Payload {
32 service: String,
33 route: String,
34 severity: Severity,
35 message: String,
36 code: u16,
37 #[serde(skip_serializing_if = "Option::is_none")]
38 timestamp: Option<String>,
39 #[serde(skip_serializing_if = "Option::is_none")]
40 context: Option<String>,
41}
42
43impl Payload {
44 fn new(
46 service: impl Into<String>,
47 route: impl Into<String>,
48 severity: Severity,
49 message: impl Into<String>,
50 code: u16,
51 context: Option<JsonValue>,
52 ) -> Self {
53 Payload {
54 service: service.into(),
55 route: route.into(),
56 severity,
57 message: message.into(),
58 code,
59 timestamp: None, context: context
61 .and_then(|v| serde_json::to_string(&v).ok())
62 .filter(|s| !s.is_empty() && s != "{}"),
63 }
64 }
65}
66
67pub async fn save_log(
109 env: &Env,
110 url: &str,
111 message: &str,
112 code: u16,
113 data: &JsonValue,
114 severity: Severity,
115) -> AnyhowResult<()> {
116 let service_name = env
118 .var("SERVICE_NAME")
119 .map(|v| v.to_string())
120 .unwrap_or_else(|_| "unknown".to_string());
121
122 let logger_url = env
124 .var("LOGGER_URL")
125 .context("LOGGER_URL environment variable not set")?
126 .to_string();
127
128 let payload = Payload::new(
130 service_name,
131 url,
132 severity,
133 message,
134 code,
135 Some(data.clone()).filter(|v| !v.is_null() && v != &json!({})),
136 );
137
138 let payload = Payload {
140 timestamp: Some(chrono::Utc::now().to_rfc3339()),
141 ..payload
142 };
143
144 let body = serde_json::to_string(&payload).context("Failed to serialize log payload")?;
146
147 let mut headers = worker::Headers::new();
149 headers
150 .append("Content-Type", "application/json")
151 .context("Failed to set Content-Type header")?;
152
153 let req = Request::new_with_init(
154 &format!("{}/log", logger_url),
155 &RequestInit {
156 method: Method::Post,
157 body: Some(worker::wasm_bindgen::JsValue::from_str(&body)),
158 headers,
159 ..Default::default()
160 },
161 )
162 .context("Failed to create HTTP request")?;
163
164 let mut resp = Fetch::Request(req)
166 .send()
167 .await
168 .context("Failed to send log request")?;
169
170 if !(200..=299).contains(&resp.status_code()) {
172 let status = resp.status_code();
173 let text = resp
174 .text()
175 .await
176 .unwrap_or_else(|_| "No response body".to_string());
177 console_error!("Failed to send log: status {}, response: {}", status, text);
178 return Err(anyhow::anyhow!(
179 "Log request failed with status {}: {}",
180 status,
181 text
182 ));
183 }
184
185 Ok(())
186}