use std::collections::HashMap;
use std::sync::{Arc, Mutex, OnceLock};
use arc_swap::ArcSwapOption;
use claviron_logging::pipeline::{
ChannelId, Level, LogChannels, Record, Value,
};
use ext_php_rs::args::Arg;
use ext_php_rs::builders::FunctionBuilder;
use ext_php_rs::flags::DataType;
use ext_php_rs::types::{ArrayKey, ZendHashTable, Zval};
use ext_php_rs::zend::ExecuteData;
use crate::extension::FunctionEntries;
const MACHINERY: [&str; 4] =
["Monolog\\", "Illuminate\\Log\\", "Psr\\Log\\", "Claviron\\Monolog\\"];
static CHANNELS: OnceLock<ArcSwapOption<LogChannels>> = OnceLock::new();
static SKIP: OnceLock<Mutex<HashMap<ChannelId, Vec<String>>>> = OnceLock::new();
fn channels_slot() -> &'static ArcSwapOption<LogChannels> {
CHANNELS.get_or_init(ArcSwapOption::const_empty)
}
pub(crate) fn configure(channels: Option<Arc<LogChannels>>) {
channels_slot().store(channels);
}
pub(crate) fn append_function_entries(
entries: &mut FunctionEntries
) -> Result<(), String> {
entries.push(
FunctionBuilder::new("claviron_log_channel", channel_handler)
.arg(Arg::new("name", DataType::String))
.not_required()
.arg(Arg::new("skip", DataType::Array))
.returns(DataType::Long, false, true)
.build()
.map_err(|error| format!("claviron_log_channel: {error}"))?,
);
entries.push(
FunctionBuilder::new("claviron_log", log_handler)
.arg(Arg::new("channel", DataType::Long))
.arg(Arg::new("level", DataType::String))
.arg(Arg::new("message", DataType::String))
.not_required()
.arg(Arg::new("context", DataType::Array))
.arg(Arg::new("target", DataType::String))
.returns(DataType::Bool, false, false)
.build()
.map_err(|error| format!("claviron_log: {error}"))?,
);
Ok(())
}
extern "C" fn channel_handler(
ex: &mut ExecuteData,
retval: &mut Zval,
) {
ext_php_rs::zend::run_handler(std::panic::AssertUnwindSafe(|| {
let mut name = Arg::new("name", DataType::String);
let mut skip = Arg::new("skip", DataType::Array);
if ex
.parser()
.arg(&mut name)
.not_required()
.arg(&mut skip)
.parse()
.is_err()
{
retval.set_null();
return;
}
let Ok(name) = name.consume::<String>() else {
retval.set_null();
return;
};
let Some(channel) = channels_slot()
.load_full()
.and_then(|channels| channels.channel(&name))
else {
retval.set_null();
return;
};
if let Some(prefixes) = skip.zval().and_then(|zval| zval.array()) {
let prefixes: Vec<String> =
prefixes.values().filter_map(|value| value.string()).collect();
SKIP.get_or_init(Default::default)
.lock()
.unwrap_or_else(|poison| poison.into_inner())
.insert(channel, prefixes);
}
retval.set_long(i64::from(channel.raw()));
}));
}
extern "C" fn log_handler(
ex: &mut ExecuteData,
retval: &mut Zval,
) {
ext_php_rs::zend::run_handler(std::panic::AssertUnwindSafe(|| {
retval.set_bool(log(ex).is_some());
}));
}
fn log(ex: &mut ExecuteData) -> Option<()> {
let above = ex.prev_execute_data.cast_const();
let mut channel = Arg::new("channel", DataType::Long);
let mut level = Arg::new("level", DataType::String);
let mut message = Arg::new("message", DataType::String);
let mut context = Arg::new("context", DataType::Array);
let mut target = Arg::new("target", DataType::String);
ex.parser()
.arg(&mut channel)
.arg(&mut level)
.arg(&mut message)
.not_required()
.arg(&mut context)
.arg(&mut target)
.parse()
.ok()?;
let channel = ChannelId::from_raw(
u16::try_from(channel.consume::<i64>().ok()?).ok()?,
);
let channels = channels_slot().load_full()?;
let level_name = level.consume::<String>().ok()?;
let message = message.consume::<String>().ok()?;
let target = match target.zval().and_then(|zval| zval.string()) {
Some(target) => target,
None => caller(unsafe { above.as_ref() }, channel),
};
let mut owned: Vec<(String, OwnedField)> = Vec::new();
if let Some(context) = context.zval().and_then(|zval| zval.array()) {
flatten(context, &mut owned);
}
let (level, lossy) = map_level(&level_name);
if lossy {
owned.push((
"psr_level".to_owned(),
OwnedField::Str(level_name.clone()),
));
}
let fields: Vec<(&str, Value<'_>)> = owned
.iter()
.map(|(key, value)| (key.as_str(), value.borrow()))
.collect();
let record =
Record { level, target: &target, message: &message, fields: &fields };
channels.emit(channel, &record).ok()
}
fn map_level(name: &str) -> (Level, bool) {
match name.to_ascii_lowercase().as_str() {
"debug" => (Level::Debug, false),
"info" => (Level::Info, false),
"notice" => (Level::Info, true),
"warning" => (Level::Warn, false),
"error" => (Level::Error, false),
"critical" | "alert" | "emergency" => (Level::Error, true),
_ => (Level::Info, true),
}
}
enum OwnedField {
Str(String),
I64(i64),
F64(f64),
Bool(bool),
}
impl OwnedField {
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),
}
}
}
fn flatten(
table: &ZendHashTable,
out: &mut Vec<(String, OwnedField)>,
) {
flatten_into(table, "", out);
}
fn flatten_into(
table: &ZendHashTable,
prefix: &str,
out: &mut Vec<(String, OwnedField)>,
) {
for (key, value) in table.iter() {
let key = match key {
ArrayKey::String(key) => key,
other => other.to_string(),
};
let key =
if prefix.is_empty() { key } else { format!("{prefix}.{key}") };
if let Some(nested) = value.array() {
flatten_into(nested, &key, out);
continue;
}
let field = if let Some(flag) = value.bool() {
OwnedField::Bool(flag)
} else if let Some(number) = value.long() {
OwnedField::I64(number)
} else if let Some(number) = value.double() {
OwnedField::F64(number)
} else if let Some(text) = value.string() {
OwnedField::Str(text)
} else if value.is_null() {
OwnedField::Str(String::new())
} else if let Some(object) = value.object() {
OwnedField::Str(format!(
"[object {}]",
object.get_class_name().unwrap_or_else(|_| "?".to_owned())
))
} else {
continue;
};
out.push((key, field));
}
}
fn caller(
above: Option<&ExecuteData>,
channel: ChannelId,
) -> String {
let skip = SKIP
.get()
.and_then(|skip| {
skip.lock().ok().and_then(|skip| skip.get(&channel).cloned())
})
.unwrap_or_default();
let mut frame = above;
while let Some(current) = frame {
if let Some((class, function)) = frame_name(current) {
let machinery = class.as_deref().is_some_and(|class| {
MACHINERY.iter().any(|prefix| class.starts_with(prefix))
|| skip
.iter()
.any(|prefix| class.starts_with(prefix.as_str()))
});
if !machinery {
return match class {
Some(class) => format!("{class}::{function}"),
None => function,
};
}
}
frame = current.previous();
}
String::new()
}
fn frame_name(frame: &ExecuteData) -> Option<(Option<String>, String)> {
let function = frame.function()?;
let (name, scope) = unsafe {
(function.common.function_name.as_ref(), function.common.scope.as_ref())
};
let name = name?.as_str().ok()?.to_owned();
let class = scope.and_then(|scope| scope.name()).map(ToOwned::to_owned);
Some((class, name))
}