use std::{
ffi::{c_char, c_void},
ptr,
};
use log::Level;
use open62541_sys::{UA_LogCategory, UA_LogLevel, UA_Logger, UA_String_vformat, va_list_};
use crate::{DataType as _, Error, Result, ua};
const LOG_TARGET: &str = "open62541_sys";
pub(crate) fn logger() -> ua::Logger {
unsafe extern "C" fn log_c(
_log_context: *mut c_void,
level: UA_LogLevel,
category: UA_LogCategory,
msg: *const c_char,
args: va_list_,
) {
let level = match level {
UA_LogLevel::UA_LOGLEVEL_FATAL | UA_LogLevel::UA_LOGLEVEL_ERROR => Level::Error,
UA_LogLevel::UA_LOGLEVEL_WARNING => Level::Warn,
UA_LogLevel::UA_LOGLEVEL_INFO => Level::Info,
UA_LogLevel::UA_LOGLEVEL_DEBUG => Level::Debug,
UA_LogLevel::UA_LOGLEVEL_TRACE => Level::Trace,
#[expect(clippy::match_same_arms, reason = "distinction of cases")]
_ => Level::Error,
};
if !log::log_enabled!(target: LOG_TARGET, level) {
return;
}
let msg = format_message(msg, args);
let msg = match msg {
Ok(ref msg) => msg.as_str().unwrap_or("Invalid log message"),
Err(_) => "Unknown log message",
};
let category = log_category(&category);
log::log!(target: LOG_TARGET, level, "({category}) {msg}");
}
unsafe extern "C" fn clear_c(logger: *mut UA_Logger) {
log::debug!("Clearing `log` logger");
let logger = unsafe { Box::from_raw(logger) };
#[expect(unpredictable_function_pointer_comparisons, reason = "extern 'C'")]
{
debug_assert!(logger.log.is_some_and(|log| log == log_c));
debug_assert!(logger.clear.is_some_and(|clear| clear == clear_c));
}
debug_assert!(logger.context.is_null());
drop(logger);
}
log::debug!("Creating `log` logger");
let logger = Box::leak(Box::new(UA_Logger {
log: Some(log_c),
context: ptr::null_mut(),
clear: Some(clear_c),
}));
unsafe { ua::Logger::from_raw(logger) }
}
const FORMAT_MESSAGE_BUFFER_LEN: usize = 512;
fn format_message(msg: *const c_char, args: open62541_sys::va_list_) -> Result<ua::String> {
let mut msg_buffer = ua::String::uninit(FORMAT_MESSAGE_BUFFER_LEN);
let status_code =
ua::StatusCode::new(unsafe { UA_String_vformat(msg_buffer.as_mut_ptr(), msg, args) });
if status_code == ua::StatusCode::BADENCODINGLIMITSEXCEEDED {
if let Some(msg_buffer) = msg_buffer.as_mut_bytes() {
for char in msg_buffer.iter_mut().rev().take(3) {
*char = b'.';
}
}
} else {
Error::verify_good(&status_code)?;
}
Ok(msg_buffer)
}
const LOG_CATEGORY_NETWORK: &str = "network";
const LOG_CATEGORY_SECURECHANNEL: &str = "channel";
const LOG_CATEGORY_SESSION: &str = "session";
const LOG_CATEGORY_SERVER: &str = "server";
const LOG_CATEGORY_CLIENT: &str = "client";
const LOG_CATEGORY_USERLAND: &str = "userland";
const LOG_CATEGORY_SECURITYPOLICY: &str = "security";
const LOG_CATEGORY_EVENTLOOP: &str = "eventloop";
const LOG_CATEGORY_PUBSUB: &str = "pubsub";
const LOG_CATEGORY_DISCOVERY: &str = "discovery";
const LOG_CATEGORY_UNKNOWN: &str = "unknown";
const fn log_category(category: &UA_LogCategory) -> &'static str {
match *category {
UA_LogCategory::UA_LOGCATEGORY_NETWORK => LOG_CATEGORY_NETWORK,
UA_LogCategory::UA_LOGCATEGORY_SECURECHANNEL => LOG_CATEGORY_SECURECHANNEL,
UA_LogCategory::UA_LOGCATEGORY_SESSION => LOG_CATEGORY_SESSION,
UA_LogCategory::UA_LOGCATEGORY_SERVER => LOG_CATEGORY_SERVER,
UA_LogCategory::UA_LOGCATEGORY_CLIENT => LOG_CATEGORY_CLIENT,
UA_LogCategory::UA_LOGCATEGORY_USERLAND => LOG_CATEGORY_USERLAND,
UA_LogCategory::UA_LOGCATEGORY_SECURITYPOLICY => LOG_CATEGORY_SECURITYPOLICY,
UA_LogCategory::UA_LOGCATEGORY_EVENTLOOP => LOG_CATEGORY_EVENTLOOP,
UA_LogCategory::UA_LOGCATEGORY_PUBSUB => LOG_CATEGORY_PUBSUB,
UA_LogCategory::UA_LOGCATEGORY_DISCOVERY => LOG_CATEGORY_DISCOVERY,
_ => LOG_CATEGORY_UNKNOWN,
}
}