From 8af61a658677ed0eef408d4e834e17081b8f1494 Mon Sep 17 00:00:00 2001 From: Pat Hickey Date: Wed, 27 Aug 2025 13:46:04 -0700 Subject: [PATCH] feat: rust ::log crate interoperation with nginx logs Co-Authored-By: Aleksei Bavshin --- Cargo.lock | 1 + Cargo.toml | 16 +++- src/lib.rs | 2 + src/log.rs | 5 +- src/log/interop.rs | 217 +++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 239 insertions(+), 2 deletions(-) create mode 100644 src/log/interop.rs diff --git a/Cargo.lock b/Cargo.lock index 98c34967..0eb45c02 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -597,6 +597,7 @@ dependencies = [ "allocator-api2", "async-task", "lock_api", + "log", "nginx-sys", "pin-project-lite", "tempfile", diff --git a/Cargo.toml b/Cargo.toml index fd1021f8..34685875 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,9 +42,10 @@ async-task = { version = "4.7.1", optional = true } lock_api = "0.4.13" nginx-sys = { path = "nginx-sys", version = "0.5.0-beta"} pin-project-lite = { version = "0.2.16", optional = true } +log = { version = "0.4.27", optional = true } [features] -default = ["std"] +default = ["std", "log"] # Enables a minimal async runtime built on top of the NGINX event loop. async = [ "alloc", @@ -65,6 +66,19 @@ std = [ # Enables the build scripts to build a copy of nginx source and link against it. vendored = ["nginx-sys/vendored"] +# Enables interop with log crate. info! and above levels always enabled, +# and debug! enabled when nginx is configured --with-debug. +log = [ + "std", + "dep:log" +] +# Enables interop with log crate. debug! enabled regardless of +# nginx configuration. +log-debug = [ "log" ] +# Enables interop with log crate. trace! and debug! enabled regardless of +# nginx configuration. +log-trace = [ "log", "log-debug" ] + [badges] maintenance = { status = "experimental" } diff --git a/src/lib.rs b/src/lib.rs index 6eb1b78b..0b7a23e5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -120,6 +120,8 @@ #![no_std] #[cfg(feature = "alloc")] extern crate alloc; +#[cfg(feature = "std")] +extern crate std; pub mod allocator; #[cfg(feature = "async")] diff --git a/src/log.rs b/src/log.rs index 47b496eb..d18ccfb8 100644 --- a/src/log.rs +++ b/src/log.rs @@ -3,6 +3,9 @@ use core::fmt::{self, Write}; use core::mem::MaybeUninit; use core::ptr::NonNull; +#[cfg(feature = "log")] +pub mod interop; + use crate::ffi::{self, ngx_err_t, ngx_log_t, ngx_uint_t, NGX_MAX_ERROR_STR}; /// This constant is set to `true` if NGINX is compiled with debug logging (`--with-debug`). @@ -193,7 +196,7 @@ macro_rules! ngx_log_debug_mask { /// Debug masks for use with [`ngx_log_debug_mask`], these represent the only accepted values for /// the mask. -#[derive(Debug)] +#[derive(Debug, Clone, Copy)] pub enum DebugMask { /// Aligns to the NGX_LOG_DEBUG_CORE mask. Core, diff --git a/src/log/interop.rs b/src/log/interop.rs new file mode 100644 index 00000000..0cb96936 --- /dev/null +++ b/src/log/interop.rs @@ -0,0 +1,217 @@ +//! Interoperation with the [::log] crate's logging macros. +//! +//! An nginx module using ngx must run [`init`] on the main thread +//! in order for [::log] macros to log to the cycle logger. +//! +//! Logging from outside of the nginx main thread is not supported, because +//! Nginx does not provide any facilities for mutual exclusion of its logging +//! interfaces. If log is used from outside of the main thread, those will be +//! dropped, and the next use of log on main thread will attempt to log a +//! warning. +//! +//! ## Crate feature flags and logging levels +//! +//! The [::log] crate defines the logging levels, in ascending order, as: +//! `error`, `warn`, `info`, `debug`, and `trace`. +//! +//! Nginx defines logging levels as `ERROR`, `WARN`, `INFO`, and `DEBUG`. The +//! [::log] crate's `trace` level is mapped to Nginx's `DEBUG` level, and all +//! others are mapped according to their name. +//! +//! The maximum level logged is determined by this crate's feature flags: +//! +//! * `log` is a default feature. Iff nginx is configured `--with-debug`, +//! `debug` is the maximum log level, otherwise `info` is the maximum level. +//! * `log-debug` implies `log`, and sets the maximum log level to `debug`, +//! regardless of nginx's configuration. +//! * `log-trace` implies `log-debug`, and sets the maximum log level to +//! `trace`, regardless of nginx's configuration. + +use core::cell::Cell; +use core::ptr::NonNull; + +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::OnceLock; +use std::thread_local; + +use crate::ffi::{ngx_log_t, ngx_uint_t, NGX_LOG_DEBUG_CORE}; +use crate::log::{log_debug, log_error, ngx_cycle_log, write_fmt, DebugMask, LOG_BUFFER_SIZE}; + +static NGX_LOGGER: Logger = Logger; +static NGX_LOGGER_NONE_USED: AtomicBool = AtomicBool::new(false); +static NGX_LOGGER_NONE_REPORTED: AtomicBool = AtomicBool::new(false); + +thread_local! { + static NGX_THREAD_LOGGER: Cell = const { Cell::new(Inner::None) }; +} + +#[derive(Copy, Clone, PartialEq, Eq)] +enum Inner { + None, + Cycle, + Specific(ngx_uint_t, NonNull), +} + +#[inline] +fn to_ngx_level(value: ::log::Level) -> ngx_uint_t { + match value { + ::log::Level::Error => nginx_sys::NGX_LOG_ERR as _, + ::log::Level::Warn => nginx_sys::NGX_LOG_WARN as _, + ::log::Level::Info => nginx_sys::NGX_LOG_INFO as _, + ::log::Level::Debug => nginx_sys::NGX_LOG_DEBUG as _, + ::log::Level::Trace => nginx_sys::NGX_LOG_DEBUG as _, + } +} + +/// Logger implementation for the [::log] facade +pub struct Logger; + +pub(crate) struct LogScope(Inner); + +impl Drop for LogScope { + fn drop(&mut self) { + NGX_THREAD_LOGGER.replace(self.0); + } +} + +/// Initializes nginx implementation for the [::log] facade. +pub fn init() { + static INIT: OnceLock<&Logger> = OnceLock::new(); + + INIT.get_or_init(|| { + NGX_THREAD_LOGGER.set(Inner::Cycle); + ::log::set_logger(&NGX_LOGGER).unwrap(); + if cfg!(feature = "log-trace") { + ::log::set_max_level(::log::LevelFilter::Trace); + } else if cfg!(ngx_feature = "debug") || cfg!(feature = "log-debug") { + ::log::set_max_level(::log::LevelFilter::Debug); + } else { + ::log::set_max_level(::log::LevelFilter::Info); + } + &NGX_LOGGER + }); +} + +impl Logger { + pub(crate) fn enter(target: DebugMask, log: NonNull) -> LogScope + where + LogScope: From, + { + init(); + let target: u32 = target.into(); + LogScope(NGX_THREAD_LOGGER.replace(Inner::Specific(target as ngx_uint_t, log))) + } + + fn current(&self) -> Inner { + NGX_THREAD_LOGGER.get() + } +} + +impl ::log::Log for Logger { + fn enabled(&self, metadata: &::log::Metadata) -> bool { + let (mask, log) = match self.current() { + Inner::None => return false, + Inner::Cycle => (NGX_LOG_DEBUG_CORE as _, ngx_cycle_log()), + Inner::Specific(mask, ptr) => (mask, ptr), + }; + + let log_level = unsafe { log.as_ref().log_level }; + + if metadata.level() < ::log::Level::Debug { + to_ngx_level(metadata.level()) < log_level + } else { + log_level & mask != 0 + } + } + + fn log(&self, record: &::log::Record) { + if self.current() == Inner::None { + NGX_LOGGER_NONE_USED.store(true, Ordering::Relaxed); + return; + } + + if !self.enabled(record.metadata()) { + return; + } + + let log = match self.current() { + Inner::Cycle => ngx_cycle_log(), + Inner::Specific(_, ptr) => ptr, + Inner::None => unreachable!(), + }; + + let mut buf = [const { ::core::mem::MaybeUninit::::uninit() }; LOG_BUFFER_SIZE]; + let message = write_fmt(&mut buf, *record.args()); + + if NGX_LOGGER_NONE_USED.load(Ordering::Relaxed) + && !NGX_LOGGER_NONE_REPORTED.load(Ordering::Relaxed) + { + unsafe { + log_error( + ::nginx_sys::NGX_LOG_WARN as _, + log.as_ptr(), + 0, + "ngx::log::interop used off main thread, and messages were dropped".as_bytes(), + ) + }; + NGX_LOGGER_NONE_REPORTED.store(true, Ordering::Relaxed); + } + + if record.level() < ::log::Level::Debug { + unsafe { log_error(to_ngx_level(record.level()), log.as_ptr(), 0, message) } + } else { + unsafe { log_debug(log.as_ptr(), 0, message) } + } + } + + fn flush(&self) {} +} + +/// Runs a closure with [`::log`] output sent to a specific instance of the nginx logger. +#[inline(always)] +pub fn with_log(target: DebugMask, log: NonNull, func: F) -> R +where + F: FnOnce() -> R, +{ + let _scope = Logger::enter(target, log); + func() +} + +#[cfg(feature = "async")] +mod async_ { + use crate::log::{ngx_log_t, DebugMask}; + + use core::future::Future; + use core::pin::Pin; + use core::ptr::NonNull; + use core::task::{Context, Poll}; + + /// Instrument a [`Future`] with [::log] output sent to a specific + /// instance of the nginx logger. + pub fn instrument_log(target: DebugMask, log: NonNull, fut: F) -> LogFut + where + F: Future, + { + LogFut { target, log, fut } + } + pin_project_lite::pin_project! { + /// Wrapper for a [`Future`] created by [`instrument_log`]. + pub struct LogFut { + target: DebugMask, + log: NonNull, + #[pin] + fut: F, + } + } + impl Future for LogFut { + type Output = F::Output; + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + let target = self.target; + let log = self.log; + let this = self.project(); + super::with_log(target, log, || this.fut.poll(cx)) + } + } +} +#[cfg(feature = "async")] +pub use self::async_::{instrument_log, LogFut};