use mlua::{Lua, LuaSerdeExt, Result as LuaResult, Table, Value};
const VERSION: &str = "0.1.0";
const DESCRIPTION: &str = "Lua logging bridge to Rust tracing";
const MAX_TABLE_ELEMENTS: usize = 1000;
const MAX_NESTING_DEPTH: usize = 10;
#[derive(Debug, Clone, Default)]
struct LuaCallerInfo {
source: String,
line: usize,
fn_name: String,
}
fn get_caller_info(lua: &Lua) -> LuaCallerInfo {
lua.inspect_stack(1, |debug| {
let source_info = debug.source();
let source = source_info
.short_src
.map(|s| s.to_string())
.unwrap_or_default();
let line = debug.current_line().unwrap_or(0);
let names = debug.names();
let fn_name = names.name.map(|s| s.to_string()).unwrap_or_default();
LuaCallerInfo {
source,
line,
fn_name,
}
})
.unwrap_or_default()
}
fn value_to_string(lua: &Lua, value: Value) -> String {
match value {
Value::Nil => String::new(),
Value::Boolean(b) => b.to_string(),
Value::Integer(i) => i.to_string(),
Value::Number(n) => n.to_string(),
Value::String(s) => s.to_string_lossy().to_string(),
Value::Table(ref t) => table_to_string(lua, t, &value),
_ => lua_tostring(lua, &value),
}
}
fn table_to_string(lua: &Lua, table: &Table, original_value: &Value) -> String {
let element_count = count_table_elements(table);
if element_count > MAX_TABLE_ELEMENTS {
return format!("<table: {} elements>", element_count);
}
if table_depth_exceeds(table, MAX_NESTING_DEPTH) {
return lua_tostring(lua, original_value);
}
let deserialize_options = mlua::DeserializeOptions::default().deny_recursive_tables(true);
match lua.from_value_with::<serde_json::Value>(original_value.clone(), deserialize_options) {
Ok(json_value) => match serde_json::to_string(&json_value) {
Ok(s) => s,
Err(_) => lua_tostring(lua, original_value),
},
Err(_) => lua_tostring(lua, original_value),
}
}
fn count_table_elements(table: &Table) -> usize {
table.pairs::<Value, Value>().count()
}
fn table_depth_exceeds(table: &Table, limit: usize) -> bool {
let mut stack: Vec<(Table, usize)> = vec![(table.clone(), 1)];
while let Some((t, depth)) = stack.pop() {
if depth > limit {
return true;
}
for pair in t.pairs::<Value, Value>() {
let Ok((key, value)) = pair else { continue };
if let Value::Table(kt) = key {
stack.push((kt, depth + 1));
}
if let Value::Table(vt) = value {
stack.push((vt, depth + 1));
}
}
}
false
}
fn lua_tostring(lua: &Lua, value: &Value) -> String {
let tostring: Result<mlua::Function, _> = lua.globals().get("tostring");
match tostring {
Ok(func) => match func.call::<String>(value.clone()) {
Ok(s) => s,
Err(_) => "<unconvertible value>".to_string(),
},
Err(_) => "<unconvertible value>".to_string(),
}
}
pub fn register(lua: &Lua) -> LuaResult<Table> {
let module = lua.create_table()?;
module.set("_VERSION", VERSION)?;
module.set("_DESCRIPTION", DESCRIPTION)?;
module.set("trace", lua.create_function(log_trace)?)?;
module.set("debug", lua.create_function(log_debug)?)?;
module.set("info", lua.create_function(log_info)?)?;
module.set("warn", lua.create_function(log_warn)?)?;
module.set("error", lua.create_function(log_error)?)?;
Ok(module)
}
macro_rules! log_fn {
($fn_name:ident => $level:ident) => {
#[doc = concat!("Log at `", stringify!($level), "` level.")]
fn $fn_name(lua: &Lua, value: Value) -> LuaResult<()> {
let msg = value_to_string(lua, value);
let caller = get_caller_info(lua);
tracing::$level!(
lua_source = %caller.source,
lua_line = caller.line,
lua_fn = %caller.fn_name,
"{}",
msg
);
Ok(())
}
};
}
log_fn!(log_trace => trace);
log_fn!(log_debug => debug);
log_fn!(log_info => info);
log_fn!(log_warn => warn);
log_fn!(log_error => error);