1#![forbid(unsafe_code)]
28#![deny(missing_docs)]
29
30use rlg::log::Log;
31use rlg::log_format::LogFormat;
32use rlg::log_level::LogLevel;
33use std::str::FromStr;
34
35#[cfg(target_arch = "wasm32")]
36use wasm_bindgen::prelude::*;
37
38#[cfg(target_arch = "wasm32")]
43#[wasm_bindgen]
44unsafe extern "C" {
45 #[wasm_bindgen(js_namespace = console, js_name = log)]
46 fn console_log(s: &str);
47 #[wasm_bindgen(js_namespace = console, js_name = warn)]
48 fn console_warn(s: &str);
49 #[wasm_bindgen(js_namespace = console, js_name = error)]
50 fn console_error(s: &str);
51}
52
53fn dispatch(level: LogLevel, rendered: &str) {
54 #[cfg(target_arch = "wasm32")]
55 {
56 match level {
57 LogLevel::WARN => console_warn(rendered),
58 LogLevel::ERROR | LogLevel::FATAL | LogLevel::CRITICAL => {
59 console_error(rendered)
60 }
61 _ => console_log(rendered),
62 }
63 }
64 #[cfg(not(target_arch = "wasm32"))]
65 {
66 let _ = level;
67 eprintln!("{rendered}");
68 }
69}
70
71#[cfg_attr(target_arch = "wasm32", wasm_bindgen)]
78#[derive(Debug, Clone)]
79pub struct RlgWasm {
80 component: String,
81 format: LogFormat,
82}
83
84#[cfg_attr(target_arch = "wasm32", wasm_bindgen)]
85impl RlgWasm {
86 #[cfg_attr(target_arch = "wasm32", wasm_bindgen(constructor))]
92 #[must_use]
93 pub fn new(component: &str, format: &str) -> Self {
94 Self {
95 component: component.to_string(),
96 format: LogFormat::from_str(format)
97 .unwrap_or(LogFormat::JSON),
98 }
99 }
100
101 pub fn info(&self, message: &str, attributes_json: Option<String>) {
105 self.emit(LogLevel::INFO, message, attributes_json.as_deref());
106 }
107
108 pub fn warn(&self, message: &str, attributes_json: Option<String>) {
110 self.emit(LogLevel::WARN, message, attributes_json.as_deref());
111 }
112
113 pub fn error(
115 &self,
116 message: &str,
117 attributes_json: Option<String>,
118 ) {
119 self.emit(LogLevel::ERROR, message, attributes_json.as_deref());
120 }
121
122 pub fn debug(
124 &self,
125 message: &str,
126 attributes_json: Option<String>,
127 ) {
128 self.emit(LogLevel::DEBUG, message, attributes_json.as_deref());
129 }
130
131 fn emit(
132 &self,
133 level: LogLevel,
134 message: &str,
135 attributes_json: Option<&str>,
136 ) {
137 let mut log = Log::build(level, message)
138 .component(&self.component)
139 .format(self.format);
140 if let Some(s) = attributes_json
141 && let Ok(serde_json::Value::Object(map)) =
142 serde_json::from_str::<serde_json::Value>(s)
143 {
144 for (k, v) in map {
145 log = log.with(&k, v);
146 }
147 }
148 let rendered = format!("{log}");
149 dispatch(level, &rendered);
150 }
151}
152
153#[cfg(test)]
154mod tests {
155 use super::*;
156
157 #[test]
158 fn new_with_unknown_format_falls_back_to_json() {
159 let r = RlgWasm::new("worker", "NotAFormat");
160 assert_eq!(r.format, LogFormat::JSON);
161 assert_eq!(r.component, "worker");
162 }
163
164 #[test]
165 fn new_accepts_each_format_variant() {
166 for name in [
167 "CLF", "CEF", "ELF", "W3C", "JSON", "GELF", "Logstash",
168 "NDJSON", "MCP", "OTLP", "Logfmt", "ECS",
169 ] {
170 let r = RlgWasm::new("svc", name);
171 assert_eq!(format!("{:?}", r.format), name);
172 }
173 }
174
175 #[test]
176 fn info_emits_without_panic() {
177 let r = RlgWasm::new("worker", "Logfmt");
178 r.info("hello", None);
179 r.info(
180 "with attrs",
181 Some(r#"{"user_id":42,"region":"eu-west-1"}"#.to_string()),
182 );
183 }
184
185 #[test]
186 fn invalid_attributes_json_is_silently_dropped() {
187 let r = RlgWasm::new("worker", "JSON");
188 r.warn("malformed input", Some("not json".to_string()));
191 }
192
193 #[test]
194 fn level_shortcuts_compose() {
195 let r = RlgWasm::new("svc", "JSON");
196 r.debug("d", None);
197 r.info("i", None);
198 r.warn("w", None);
199 r.error("e", None);
200 }
201
202 #[test]
203 fn rlg_wasm_is_clone() {
204 let a = RlgWasm::new("svc", "JSON");
205 let b = a.clone();
206 assert_eq!(a.component, b.component);
207 assert_eq!(a.format, b.format);
208 }
209}