#![forbid(unsafe_code)]
#![deny(missing_docs)]
use rlg::log::Log;
use rlg::log_format::LogFormat;
use rlg::log_level::LogLevel;
use std::str::FromStr;
#[cfg(target_arch = "wasm32")]
use wasm_bindgen::prelude::*;
#[cfg(target_arch = "wasm32")]
#[wasm_bindgen]
unsafe extern "C" {
#[wasm_bindgen(js_namespace = console, js_name = log)]
fn console_log(s: &str);
#[wasm_bindgen(js_namespace = console, js_name = warn)]
fn console_warn(s: &str);
#[wasm_bindgen(js_namespace = console, js_name = error)]
fn console_error(s: &str);
}
fn dispatch(level: LogLevel, rendered: &str) {
#[cfg(target_arch = "wasm32")]
{
match level {
LogLevel::WARN => console_warn(rendered),
LogLevel::ERROR | LogLevel::FATAL | LogLevel::CRITICAL => {
console_error(rendered)
}
_ => console_log(rendered),
}
}
#[cfg(not(target_arch = "wasm32"))]
{
let _ = level;
eprintln!("{rendered}");
}
}
#[cfg_attr(target_arch = "wasm32", wasm_bindgen)]
#[derive(Debug, Clone)]
pub struct RlgWasm {
component: String,
format: LogFormat,
}
#[cfg_attr(target_arch = "wasm32", wasm_bindgen)]
impl RlgWasm {
#[cfg_attr(target_arch = "wasm32", wasm_bindgen(constructor))]
#[must_use]
pub fn new(component: &str, format: &str) -> Self {
Self {
component: component.to_string(),
format: LogFormat::from_str(format)
.unwrap_or(LogFormat::JSON),
}
}
pub fn info(&self, message: &str, attributes_json: Option<String>) {
self.emit(LogLevel::INFO, message, attributes_json.as_deref());
}
pub fn warn(&self, message: &str, attributes_json: Option<String>) {
self.emit(LogLevel::WARN, message, attributes_json.as_deref());
}
pub fn error(
&self,
message: &str,
attributes_json: Option<String>,
) {
self.emit(LogLevel::ERROR, message, attributes_json.as_deref());
}
pub fn debug(
&self,
message: &str,
attributes_json: Option<String>,
) {
self.emit(LogLevel::DEBUG, message, attributes_json.as_deref());
}
fn emit(
&self,
level: LogLevel,
message: &str,
attributes_json: Option<&str>,
) {
let mut log = Log::build(level, message)
.component(&self.component)
.format(self.format);
if let Some(s) = attributes_json
&& let Ok(serde_json::Value::Object(map)) =
serde_json::from_str::<serde_json::Value>(s)
{
for (k, v) in map {
log = log.with(&k, v);
}
}
let rendered = format!("{log}");
dispatch(level, &rendered);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn new_with_unknown_format_falls_back_to_json() {
let r = RlgWasm::new("worker", "NotAFormat");
assert_eq!(r.format, LogFormat::JSON);
assert_eq!(r.component, "worker");
}
#[test]
fn new_accepts_each_format_variant() {
for name in [
"CLF", "CEF", "ELF", "W3C", "JSON", "GELF", "Logstash",
"NDJSON", "MCP", "OTLP", "Logfmt", "ECS",
] {
let r = RlgWasm::new("svc", name);
assert_eq!(format!("{:?}", r.format), name);
}
}
#[test]
fn info_emits_without_panic() {
let r = RlgWasm::new("worker", "Logfmt");
r.info("hello", None);
r.info(
"with attrs",
Some(r#"{"user_id":42,"region":"eu-west-1"}"#.to_string()),
);
}
#[test]
fn invalid_attributes_json_is_silently_dropped() {
let r = RlgWasm::new("worker", "JSON");
r.warn("malformed input", Some("not json".to_string()));
}
#[test]
fn level_shortcuts_compose() {
let r = RlgWasm::new("svc", "JSON");
r.debug("d", None);
r.info("i", None);
r.warn("w", None);
r.error("e", None);
}
#[test]
fn rlg_wasm_is_clone() {
let a = RlgWasm::new("svc", "JSON");
let b = a.clone();
assert_eq!(a.component, b.component);
assert_eq!(a.format, b.format);
}
}