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 logger_auth = env
130 .var("LOGGER_AUTH")
131 .context("LOGGER_AUTH environment variable not set")?
132 .to_string();
133
134 let payload = Payload::new(
136 service_name,
137 url,
138 severity,
139 message,
140 code,
141 Some(data.clone()).filter(|v| !v.is_null() && v != &json!({})),
142 );
143
144 let payload = Payload {
146 timestamp: Some(chrono::Utc::now().to_rfc3339()),
147 ..payload
148 };
149
150 let body = serde_json::to_string(&payload).context("Failed to serialize log payload")?;
152
153 let mut headers = worker::Headers::new();
155 headers
156 .append("Content-Type", "application/json")
157 .context("Failed to set Content-Type header")?;
158 headers
159 .append("Authorization", &format!("Basic {}", logger_auth))
160 .context("Failed to set Authorization header")?;
161
162 let req = Request::new_with_init(
163 &format!("{}/log", logger_url),
164 &RequestInit {
165 method: Method::Post,
166 body: Some(worker::wasm_bindgen::JsValue::from_str(&body)),
167 headers,
168 ..Default::default()
169 },
170 )
171 .context("Failed to create HTTP request")?;
172
173 let mut resp = Fetch::Request(req)
175 .send()
176 .await
177 .context("Failed to send log request")?;
178
179 if !(200..=299).contains(&resp.status_code()) {
181 let status = resp.status_code();
182 let text = resp
183 .text()
184 .await
185 .unwrap_or_else(|_| "No response body".to_string());
186 console_error!("Failed to send log: status {}, response: {}", status, text);
187 return Err(anyhow::anyhow!(
188 "Log request failed with status {}: {}",
189 status,
190 text
191 ));
192 }
193
194 Ok(())
195}