Skip to content

Commit 8af61a6

Browse files
pchickeybavshin-f5
andcommitted
feat: rust ::log crate interoperation with nginx logs
Co-Authored-By: Aleksei Bavshin <a.bavshin@f5.com>
1 parent 07ce1f7 commit 8af61a6

File tree

5 files changed

+239
-2
lines changed

5 files changed

+239
-2
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,10 @@ async-task = { version = "4.7.1", optional = true }
4242
lock_api = "0.4.13"
4343
nginx-sys = { path = "nginx-sys", version = "0.5.0-beta"}
4444
pin-project-lite = { version = "0.2.16", optional = true }
45+
log = { version = "0.4.27", optional = true }
4546

4647
[features]
47-
default = ["std"]
48+
default = ["std", "log"]
4849
# Enables a minimal async runtime built on top of the NGINX event loop.
4950
async = [
5051
"alloc",
@@ -65,6 +66,19 @@ std = [
6566
# Enables the build scripts to build a copy of nginx source and link against it.
6667
vendored = ["nginx-sys/vendored"]
6768

69+
# Enables interop with log crate. info! and above levels always enabled,
70+
# and debug! enabled when nginx is configured --with-debug.
71+
log = [
72+
"std",
73+
"dep:log"
74+
]
75+
# Enables interop with log crate. debug! enabled regardless of
76+
# nginx configuration.
77+
log-debug = [ "log" ]
78+
# Enables interop with log crate. trace! and debug! enabled regardless of
79+
# nginx configuration.
80+
log-trace = [ "log", "log-debug" ]
81+
6882
[badges]
6983
maintenance = { status = "experimental" }
7084

src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,8 @@
120120
#![no_std]
121121
#[cfg(feature = "alloc")]
122122
extern crate alloc;
123+
#[cfg(feature = "std")]
124+
extern crate std;
123125

124126
pub mod allocator;
125127
#[cfg(feature = "async")]

src/log.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ use core::fmt::{self, Write};
33
use core::mem::MaybeUninit;
44
use core::ptr::NonNull;
55

6+
#[cfg(feature = "log")]
7+
pub mod interop;
8+
69
use crate::ffi::{self, ngx_err_t, ngx_log_t, ngx_uint_t, NGX_MAX_ERROR_STR};
710

811
/// 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 {
193196

194197
/// Debug masks for use with [`ngx_log_debug_mask`], these represent the only accepted values for
195198
/// the mask.
196-
#[derive(Debug)]
199+
#[derive(Debug, Clone, Copy)]
197200
pub enum DebugMask {
198201
/// Aligns to the NGX_LOG_DEBUG_CORE mask.
199202
Core,

src/log/interop.rs

Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
//! Interoperation with the [::log] crate's logging macros.
2+
//!
3+
//! An nginx module using ngx must run [`init`] on the main thread
4+
//! in order for [::log] macros to log to the cycle logger.
5+
//!
6+
//! Logging from outside of the nginx main thread is not supported, because
7+
//! Nginx does not provide any facilities for mutual exclusion of its logging
8+
//! interfaces. If log is used from outside of the main thread, those will be
9+
//! dropped, and the next use of log on main thread will attempt to log a
10+
//! warning.
11+
//!
12+
//! ## Crate feature flags and logging levels
13+
//!
14+
//! The [::log] crate defines the logging levels, in ascending order, as:
15+
//! `error`, `warn`, `info`, `debug`, and `trace`.
16+
//!
17+
//! Nginx defines logging levels as `ERROR`, `WARN`, `INFO`, and `DEBUG`. The
18+
//! [::log] crate's `trace` level is mapped to Nginx's `DEBUG` level, and all
19+
//! others are mapped according to their name.
20+
//!
21+
//! The maximum level logged is determined by this crate's feature flags:
22+
//!
23+
//! * `log` is a default feature. Iff nginx is configured `--with-debug`,
24+
//! `debug` is the maximum log level, otherwise `info` is the maximum level.
25+
//! * `log-debug` implies `log`, and sets the maximum log level to `debug`,
26+
//! regardless of nginx's configuration.
27+
//! * `log-trace` implies `log-debug`, and sets the maximum log level to
28+
//! `trace`, regardless of nginx's configuration.
29+
30+
use core::cell::Cell;
31+
use core::ptr::NonNull;
32+
33+
use std::sync::atomic::{AtomicBool, Ordering};
34+
use std::sync::OnceLock;
35+
use std::thread_local;
36+
37+
use crate::ffi::{ngx_log_t, ngx_uint_t, NGX_LOG_DEBUG_CORE};
38+
use crate::log::{log_debug, log_error, ngx_cycle_log, write_fmt, DebugMask, LOG_BUFFER_SIZE};
39+
40+
static NGX_LOGGER: Logger = Logger;
41+
static NGX_LOGGER_NONE_USED: AtomicBool = AtomicBool::new(false);
42+
static NGX_LOGGER_NONE_REPORTED: AtomicBool = AtomicBool::new(false);
43+
44+
thread_local! {
45+
static NGX_THREAD_LOGGER: Cell<Inner> = const { Cell::new(Inner::None) };
46+
}
47+
48+
#[derive(Copy, Clone, PartialEq, Eq)]
49+
enum Inner {
50+
None,
51+
Cycle,
52+
Specific(ngx_uint_t, NonNull<ngx_log_t>),
53+
}
54+
55+
#[inline]
56+
fn to_ngx_level(value: ::log::Level) -> ngx_uint_t {
57+
match value {
58+
::log::Level::Error => nginx_sys::NGX_LOG_ERR as _,
59+
::log::Level::Warn => nginx_sys::NGX_LOG_WARN as _,
60+
::log::Level::Info => nginx_sys::NGX_LOG_INFO as _,
61+
::log::Level::Debug => nginx_sys::NGX_LOG_DEBUG as _,
62+
::log::Level::Trace => nginx_sys::NGX_LOG_DEBUG as _,
63+
}
64+
}
65+
66+
/// Logger implementation for the [::log] facade
67+
pub struct Logger;
68+
69+
pub(crate) struct LogScope(Inner);
70+
71+
impl Drop for LogScope {
72+
fn drop(&mut self) {
73+
NGX_THREAD_LOGGER.replace(self.0);
74+
}
75+
}
76+
77+
/// Initializes nginx implementation for the [::log] facade.
78+
pub fn init() {
79+
static INIT: OnceLock<&Logger> = OnceLock::new();
80+
81+
INIT.get_or_init(|| {
82+
NGX_THREAD_LOGGER.set(Inner::Cycle);
83+
::log::set_logger(&NGX_LOGGER).unwrap();
84+
if cfg!(feature = "log-trace") {
85+
::log::set_max_level(::log::LevelFilter::Trace);
86+
} else if cfg!(ngx_feature = "debug") || cfg!(feature = "log-debug") {
87+
::log::set_max_level(::log::LevelFilter::Debug);
88+
} else {
89+
::log::set_max_level(::log::LevelFilter::Info);
90+
}
91+
&NGX_LOGGER
92+
});
93+
}
94+
95+
impl Logger {
96+
pub(crate) fn enter<T>(target: DebugMask, log: NonNull<ngx_log_t>) -> LogScope
97+
where
98+
LogScope: From<T>,
99+
{
100+
init();
101+
let target: u32 = target.into();
102+
LogScope(NGX_THREAD_LOGGER.replace(Inner::Specific(target as ngx_uint_t, log)))
103+
}
104+
105+
fn current(&self) -> Inner {
106+
NGX_THREAD_LOGGER.get()
107+
}
108+
}
109+
110+
impl ::log::Log for Logger {
111+
fn enabled(&self, metadata: &::log::Metadata) -> bool {
112+
let (mask, log) = match self.current() {
113+
Inner::None => return false,
114+
Inner::Cycle => (NGX_LOG_DEBUG_CORE as _, ngx_cycle_log()),
115+
Inner::Specific(mask, ptr) => (mask, ptr),
116+
};
117+
118+
let log_level = unsafe { log.as_ref().log_level };
119+
120+
if metadata.level() < ::log::Level::Debug {
121+
to_ngx_level(metadata.level()) < log_level
122+
} else {
123+
log_level & mask != 0
124+
}
125+
}
126+
127+
fn log(&self, record: &::log::Record) {
128+
if self.current() == Inner::None {
129+
NGX_LOGGER_NONE_USED.store(true, Ordering::Relaxed);
130+
return;
131+
}
132+
133+
if !self.enabled(record.metadata()) {
134+
return;
135+
}
136+
137+
let log = match self.current() {
138+
Inner::Cycle => ngx_cycle_log(),
139+
Inner::Specific(_, ptr) => ptr,
140+
Inner::None => unreachable!(),
141+
};
142+
143+
let mut buf = [const { ::core::mem::MaybeUninit::<u8>::uninit() }; LOG_BUFFER_SIZE];
144+
let message = write_fmt(&mut buf, *record.args());
145+
146+
if NGX_LOGGER_NONE_USED.load(Ordering::Relaxed)
147+
&& !NGX_LOGGER_NONE_REPORTED.load(Ordering::Relaxed)
148+
{
149+
unsafe {
150+
log_error(
151+
::nginx_sys::NGX_LOG_WARN as _,
152+
log.as_ptr(),
153+
0,
154+
"ngx::log::interop used off main thread, and messages were dropped".as_bytes(),
155+
)
156+
};
157+
NGX_LOGGER_NONE_REPORTED.store(true, Ordering::Relaxed);
158+
}
159+
160+
if record.level() < ::log::Level::Debug {
161+
unsafe { log_error(to_ngx_level(record.level()), log.as_ptr(), 0, message) }
162+
} else {
163+
unsafe { log_debug(log.as_ptr(), 0, message) }
164+
}
165+
}
166+
167+
fn flush(&self) {}
168+
}
169+
170+
/// Runs a closure with [`::log`] output sent to a specific instance of the nginx logger.
171+
#[inline(always)]
172+
pub fn with_log<F, R>(target: DebugMask, log: NonNull<ngx_log_t>, func: F) -> R
173+
where
174+
F: FnOnce() -> R,
175+
{
176+
let _scope = Logger::enter(target, log);
177+
func()
178+
}
179+
180+
#[cfg(feature = "async")]
181+
mod async_ {
182+
use crate::log::{ngx_log_t, DebugMask};
183+
184+
use core::future::Future;
185+
use core::pin::Pin;
186+
use core::ptr::NonNull;
187+
use core::task::{Context, Poll};
188+
189+
/// Instrument a [`Future`] with [::log] output sent to a specific
190+
/// instance of the nginx logger.
191+
pub fn instrument_log<F>(target: DebugMask, log: NonNull<ngx_log_t>, fut: F) -> LogFut<F>
192+
where
193+
F: Future,
194+
{
195+
LogFut { target, log, fut }
196+
}
197+
pin_project_lite::pin_project! {
198+
/// Wrapper for a [`Future`] created by [`instrument_log`].
199+
pub struct LogFut<F> {
200+
target: DebugMask,
201+
log: NonNull<ngx_log_t>,
202+
#[pin]
203+
fut: F,
204+
}
205+
}
206+
impl<F: Future> Future for LogFut<F> {
207+
type Output = F::Output;
208+
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
209+
let target = self.target;
210+
let log = self.log;
211+
let this = self.project();
212+
super::with_log(target, log, || this.fut.poll(cx))
213+
}
214+
}
215+
}
216+
#[cfg(feature = "async")]
217+
pub use self::async_::{instrument_log, LogFut};

0 commit comments

Comments
 (0)