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};