// yt - A fully featured command line YouTube client // // Copyright (C) 2024 Benedikt Peetz // SPDX-License-Identifier: GPL-3.0-or-later // // This file is part of Yt. // // You should have received a copy of the License along with this program. // If not, see . // This file is taken from: https://github.com/dylanbstorey/pyo3-pylogger/blob/d89e0d6820ebc4f067647e3b74af59dbc4941dd5/src/lib.rs // It is licensed under the Apache 2.0 License, copyright up to 2024 by Dylan Storey // It was modified by Benedikt Peetz 2024 // The pyo3 `pyfunction` proc-macros call unsafe functions internally, which trigger this lint. #![allow(unsafe_op_in_unsafe_fn)] use std::ffi::CString; use log::{logger, Level, MetadataBuilder, Record}; use pyo3::{ prelude::{PyAnyMethods, PyListMethods, PyModuleMethods}, pyfunction, wrap_pyfunction, Bound, PyAny, PyResult, Python, }; /// Consume a Python `logging.LogRecord` and emit a Rust `Log` instead. #[allow(clippy::needless_pass_by_value)] #[pyfunction] fn host_log(record: Bound<'_, PyAny>, rust_target: &str) -> PyResult<()> { let level = record.getattr("levelno")?; let message = record.getattr("getMessage")?.call0()?.to_string(); let pathname = record.getattr("pathname")?.to_string(); let lineno = record .getattr("lineno")? .to_string() .parse::() .expect("This should always be a u32"); let logger_name = record.getattr("name")?.to_string(); let full_target: Option = if logger_name.trim().is_empty() || logger_name == "root" { None } else { // Libraries (ex: tracing_subscriber::filter::Directive) expect rust-style targets like foo::bar, // and may not deal well with "." as a module separator: let logger_name = logger_name.replace('.', "::"); Some(format!("{rust_target}::{logger_name}")) }; let target = full_target.as_deref().unwrap_or(rust_target); // error let error_metadata = if level.ge(40u8)? { MetadataBuilder::new() .target(target) .level(Level::Error) .build() } else if level.ge(30u8)? { MetadataBuilder::new() .target(target) .level(Level::Warn) .build() } else if level.ge(20u8)? { MetadataBuilder::new() .target(target) .level(Level::Info) .build() } else if level.ge(10u8)? { MetadataBuilder::new() .target(target) .level(Level::Debug) .build() } else { MetadataBuilder::new() .target(target) .level(Level::Trace) .build() }; logger().log( &Record::builder() .metadata(error_metadata) .args(format_args!("{}", &message)) .line(Some(lineno)) .file(None) .module_path(Some(&pathname)) .build(), ); Ok(()) } /// Registers the `host_log` function in rust as the event handler for Python's logging logger /// This function needs to be called from within a pyo3 context as early as possible to ensure logging messages /// arrive to the rust consumer. /// /// # Panics /// Only if internal assertions fail. #[allow(clippy::module_name_repetitions)] pub fn setup_logging(py: Python<'_>, target: &str) -> PyResult<()> { let logging = py.import("logging")?; logging.setattr("host_log", wrap_pyfunction!(host_log, &logging)?)?; py.run( CString::new(format!( r#" class HostHandler(Handler): def __init__(self, level=0): super().__init__(level=level) def emit(self, record): host_log(record,"{target}") oldBasicConfig = basicConfig def basicConfig(*pargs, **kwargs): if "handlers" not in kwargs: kwargs["handlers"] = [HostHandler()] return oldBasicConfig(*pargs, **kwargs) "# )) .expect("This is hardcoded") .as_c_str(), Some(&logging.dict()), None, )?; let all = logging.index()?; all.append("HostHandler")?; Ok(()) }