use claviron::logging::pipeline::{ChannelId, Level, Record, Value, installed};
use pyo3::prelude::*;
use pyo3::types::{PyBool, PyDict, PyFloat, PyInt, PyString};
pub(crate) const MODULE_SOURCE: &str = include_str!("log.py");
#[pyfunction]
#[pyo3(name = "_log_channel")]
pub(crate) fn log_channel(name: &str) -> Option<u16> {
installed()?.channel(name).map(ChannelId::raw)
}
#[pyfunction]
#[pyo3(name = "_log", signature = (channel, level, message, target = None, fields = None))]
pub(crate) fn log(
channel: u16,
level: &str,
message: &str,
target: Option<&str>,
fields: Option<&Bound<'_, PyDict>>,
) -> PyResult<bool> {
let Some(channels) = installed() else { return Ok(false) };
let mut owned: Vec<(String, Owned)> = Vec::new();
if let Some(fields) = fields {
for (key, value) in fields.iter() {
owned.push((key.str()?.to_string(), Owned::from_python(&value)?));
}
}
let name = level;
let (level, lossy) = map_level(name);
if lossy {
owned.push((
"psr_level".to_owned(),
Owned::Str(name.to_ascii_lowercase()),
));
}
let fields: Vec<(&str, Value<'_>)> = owned
.iter()
.map(|(key, value)| (key.as_str(), value.borrow()))
.collect();
let record = Record {
level,
target: target.unwrap_or(""),
message,
fields: &fields,
};
Ok(channels.emit(ChannelId::from_raw(channel), &record).is_ok())
}
fn map_level(name: &str) -> (Level, bool) {
match name.to_ascii_lowercase().as_str() {
"debug" => (Level::Debug, false),
"info" => (Level::Info, false),
"warning" | "warn" => (Level::Warn, false),
"error" => (Level::Error, false),
"critical" | "fatal" => (Level::Error, true),
_ => (Level::Info, true),
}
}
enum Owned {
Str(String),
I64(i64),
F64(f64),
Bool(bool),
}
impl Owned {
fn from_python(value: &Bound<'_, PyAny>) -> PyResult<Self> {
Ok(if value.is_instance_of::<PyBool>() {
Self::Bool(value.extract()?)
} else if value.is_instance_of::<PyInt>() {
match value.extract::<i64>() {
Ok(number) => Self::I64(number),
Err(_) => Self::Str(value.str()?.to_string()),
}
} else if value.is_instance_of::<PyFloat>() {
Self::F64(value.extract()?)
} else if value.is_instance_of::<PyString>() {
Self::Str(value.extract()?)
} else if value.is_none() {
Self::Str(String::new())
} else {
Self::Str(value.str()?.to_string())
})
}
fn borrow(&self) -> Value<'_> {
match self {
Self::Str(text) => Value::Str(text),
Self::I64(number) => Value::I64(*number),
Self::F64(number) => Value::F64(*number),
Self::Bool(flag) => Value::Bool(*flag),
}
}
}