nwep-rs 0.1.8

Rust bindings for the NWEP (WEB/1) protocol library
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
#![allow(unsafe_op_in_unsafe_fn)]

use crate::ffi;
use std::ffi::{CStr, CString};
use std::sync::Mutex;

/// `LogLevel` controls the verbosity of the NWEP C library's internal logging.
///
/// Levels are ordered from most verbose ([`LogLevel::Trace`]) to least verbose
/// ([`LogLevel::Error`]). When a level is set with [`set_level`], only entries
/// at that level or higher are emitted.
///
/// The numeric discriminants match the C library's `NWEP_LOG_*` constants,
/// which allows the `PartialOrd`/`Ord` impls to behave correctly for threshold
/// comparisons.
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub enum LogLevel {
    /// `Trace` is the most verbose level, intended for fine-grained internal
    /// tracing (e.g., individual packet processing steps). Only enable this
    /// during active debugging; it generates very high log volume.
    Trace = 0,
    /// `Debug` is for developer-oriented diagnostic information such as
    /// connection state transitions and header values.
    Debug = 1,
    /// `Info` is for routine operational events such as connections established
    /// and requests completed.
    Info = 2,
    /// `Warn` is for recoverable anomalies that do not prevent the operation
    /// from completing but may indicate a misconfiguration or degraded state.
    Warn = 3,
    /// `Error` is for failures that require attention. At this level only
    /// conditions that cause an operation to fail are logged.
    Error = 4,
}

impl LogLevel {
    /// `as_str` returns the canonical lowercase string representation of this
    /// log level as defined by the C library (e.g., `"trace"`, `"debug"`).
    ///
    /// The returned string is a `'static` reference into the C library's string
    /// table. Falls back to `"unknown"` if the C library returns a null
    /// pointer.
    pub fn as_str(&self) -> &'static str {
        let level = match self {
            LogLevel::Trace => ffi::nwep_log_level_NWEP_LOG_TRACE,
            LogLevel::Debug => ffi::nwep_log_level_NWEP_LOG_DEBUG,
            LogLevel::Info => ffi::nwep_log_level_NWEP_LOG_INFO,
            LogLevel::Warn => ffi::nwep_log_level_NWEP_LOG_WARN,
            LogLevel::Error => ffi::nwep_log_level_NWEP_LOG_ERROR,
        };
        unsafe {
            let ptr = ffi::nwep_log_level_str(level);
            if ptr.is_null() {
                "unknown"
            } else {
                CStr::from_ptr(ptr).to_str().unwrap_or("unknown")
            }
        }
    }

    /// `to_ffi` converts this [`LogLevel`] to the raw `nwep_log_level`
    /// discriminant expected by C library functions.
    pub(crate) fn to_ffi(&self) -> ffi::nwep_log_level {
        match self {
            LogLevel::Trace => ffi::nwep_log_level_NWEP_LOG_TRACE,
            LogLevel::Debug => ffi::nwep_log_level_NWEP_LOG_DEBUG,
            LogLevel::Info => ffi::nwep_log_level_NWEP_LOG_INFO,
            LogLevel::Warn => ffi::nwep_log_level_NWEP_LOG_WARN,
            LogLevel::Error => ffi::nwep_log_level_NWEP_LOG_ERROR,
        }
    }
}

impl From<ffi::nwep_log_level> for LogLevel {
    fn from(l: ffi::nwep_log_level) -> Self {
        match l {
            ffi::nwep_log_level_NWEP_LOG_TRACE => LogLevel::Trace,
            ffi::nwep_log_level_NWEP_LOG_DEBUG => LogLevel::Debug,
            ffi::nwep_log_level_NWEP_LOG_INFO => LogLevel::Info,
            ffi::nwep_log_level_NWEP_LOG_WARN => LogLevel::Warn,
            ffi::nwep_log_level_NWEP_LOG_ERROR => LogLevel::Error,
            _ => LogLevel::Info,
        }
    }
}

/// `LogEntry` is a single structured log record delivered to a Rust callback
/// registered with [`set_callback`].
///
/// The C library fills in all fields before invoking the callback. The Rust
/// binding copies the data out of the C struct and converts C strings to owned
/// `String` values, so the callback receives a fully owned, heap-allocated
/// entry that can be stored or forwarded without any lifetime constraints.
#[derive(Clone, Debug)]
pub struct LogEntry {
    /// `level` is the severity level of this log entry.
    pub level: LogLevel,

    /// `timestamp_ns` is the wall-clock time at which the entry was created,
    /// expressed as nanoseconds since the Unix epoch (see
    /// [`crate::types::Tstamp`]).
    pub timestamp_ns: u64,

    /// `trace_id` is the 16-byte distributed trace ID associated with the
    /// operation that produced this entry. An all-zero trace ID indicates that
    /// no trace context is available.
    pub trace_id: [u8; 16],

    /// `component` is the name of the C library subsystem that emitted this
    /// entry (e.g., `"quic"`, `"auth"`, `"trust"`).
    pub component: String,

    /// `message` is the human-readable log message text.
    pub message: String,
}

impl LogEntry {
    /// `format_json` serializes this log entry as a structured JSON object.
    ///
    /// The JSON is produced by the C library's `nwep_log_format_json` function
    /// into a 4096-byte internal buffer, then returned as an owned `String`.
    /// The output is a single-line JSON object without a trailing newline,
    /// suitable for newline-delimited JSON (NDJSON) log streams.
    ///
    /// # Example output
    ///
    /// ```json
    /// {"level":"info","timestamp_ns":1700000000000000000,"trace_id":"00000000000000000000000000000000","component":"myapp","message":"server started"}
    /// ```
    pub fn format_json(&self) -> String {
        let component = CString::new(self.component.as_str()).unwrap_or_default();
        let message = CString::new(self.message.as_str()).unwrap_or_default();
        let ffi_entry = ffi::nwep_log_entry {
            level: self.level.to_ffi(),
            timestamp_ns: self.timestamp_ns,
            trace_id: self.trace_id,
            component: component.as_ptr(),
            message: message.as_ptr(),
        };
        let mut buf = vec![0u8; 4096];
        let n = unsafe {
            ffi::nwep_log_format_json(buf.as_mut_ptr().cast(), buf.len(), &ffi_entry)
        };
        String::from_utf8_lossy(&buf[..n]).into_owned()
    }
}

/// `set_level` sets the global minimum log level.
///
/// Log entries with a level below `level` are discarded inside the C library
/// before any sink (stderr, callback) is invoked. This setting is global and
/// affects all clients and servers in the process.
///
/// # Example
///
/// ```rust
/// use nwep::log::{set_level, LogLevel};
///
/// set_level(LogLevel::Warn); // only Warn and Error messages will be shown
/// ```
pub fn set_level(level: LogLevel) {
    unsafe { ffi::nwep_log_set_level(level.to_ffi()) }
}

/// `get_level` returns the current global minimum log level.
///
/// # Example
///
/// ```rust
/// use nwep::log::{get_level, set_level, LogLevel};
///
/// set_level(LogLevel::Debug);
/// assert_eq!(get_level(), LogLevel::Debug);
/// ```
pub fn get_level() -> LogLevel {
    LogLevel::from(unsafe { ffi::nwep_log_get_level() })
}

/// `set_json` enables or disables structured JSON formatting for the stderr
/// sink.
///
/// When `enabled` is `true`, each log line written to stderr is a
/// newline-delimited JSON object produced by the same logic as
/// [`LogEntry::format_json`]. When `false`, a human-readable plain-text format
/// is used. This setting has no effect if the stderr sink is disabled (see
/// [`set_stderr`]).
pub fn set_json(enabled: bool) {
    unsafe { ffi::nwep_log_set_json(enabled as i32) }
}

/// `set_stderr` enables or disables writing log entries to standard error.
///
/// This is the simplest way to observe C library debug output during
/// development. Pass `true` to enable and `false` to disable. The stderr sink
/// and the Rust callback sink ([`set_callback`]) operate independently; both
/// can be active simultaneously.
///
/// # Example
///
/// ```rust
/// use nwep::log::{set_stderr, set_level, LogLevel};
///
/// set_level(LogLevel::Debug);
/// set_stderr(true); // all Debug+ messages will appear on stderr
/// ```
pub fn set_stderr(enabled: bool) {
    unsafe { ffi::nwep_log_set_stderr(enabled as i32) }
}

/// The type of a user-supplied log callback. The closure receives a fully owned
/// [`LogEntry`] for each log message emitted by the C library at or above the
/// current level.
type LogCallbackFn = Box<dyn Fn(LogEntry) + Send + Sync + 'static>;

/// `LOG_CALLBACK` is the global slot that holds the active Rust log callback.
///
/// A `Mutex` is used because the C library may invoke the trampoline from any
/// thread. The `Option` allows the slot to be empty when no callback is
/// installed.
static LOG_CALLBACK: Mutex<Option<LogCallbackFn>> = Mutex::new(None);

/// `log_trampoline` is the `extern "C"` function passed to the C library as
/// the log callback. It converts the raw `nwep_log_entry` pointer into a safe
/// Rust [`LogEntry`] and invokes the user closure stored in [`LOG_CALLBACK`].
///
/// If `entry` is null the trampoline returns immediately without invoking the
/// closure. The `user_data` parameter is unused; all state is carried through
/// the global `LOG_CALLBACK`.
unsafe extern "C" fn log_trampoline(
    entry: *const ffi::nwep_log_entry,
    _user_data: *mut std::ffi::c_void,
) {
    if entry.is_null() {
        return;
    }
    let e = &*entry;
    let component = if e.component.is_null() {
        String::new()
    } else {
        CStr::from_ptr(e.component).to_string_lossy().into_owned()
    };
    let message = if e.message.is_null() {
        String::new()
    } else {
        CStr::from_ptr(e.message).to_string_lossy().into_owned()
    };
    let log_entry = LogEntry {
        level: LogLevel::from(e.level),
        timestamp_ns: e.timestamp_ns,
        trace_id: e.trace_id,
        component,
        message,
    };
    if let Ok(guard) = LOG_CALLBACK.lock() {
        if let Some(cb) = guard.as_ref() {
            cb(log_entry);
        }
    }
}

/// `set_callback` installs a Rust closure as the global log callback.
///
/// The closure is called on whatever thread the C library uses to emit the log
/// entry, so it must be `Send + Sync`. Only one callback can be active at a
/// time; calling `set_callback` again atomically replaces the previous closure
/// before registering the new one with the C library.
///
/// The callback receives a fully owned [`LogEntry`] value. The closure may
/// block briefly (e.g., to acquire a lock on an output buffer) but should not
/// perform long-running I/O on the calling thread, as this may stall the C
/// library's event loop.
///
/// Use [`clear_callback`] to uninstall the callback when it is no longer
/// needed.
///
/// # Example
///
/// ```rust
/// use nwep::log::{set_callback, LogEntry};
///
/// set_callback(|entry: LogEntry| {
///     println!("[{}] {}", entry.level.as_str(), entry.message);
/// });
/// ```
pub fn set_callback<F: Fn(LogEntry) + Send + Sync + 'static>(cb: F) {
    let mut guard = LOG_CALLBACK.lock().unwrap();
    *guard = Some(Box::new(cb));
    drop(guard);
    unsafe { ffi::nwep_log_set_callback(Some(log_trampoline), std::ptr::null_mut()) }
}

/// `clear_callback` removes the active log callback and unregisters the
/// trampoline from the C library.
///
/// After this call the C library will no longer invoke any Rust code for log
/// entries. The previously installed closure is dropped.
///
/// It is safe to call `clear_callback` even if no callback is currently
/// installed.
pub fn clear_callback() {
    let mut guard = LOG_CALLBACK.lock().unwrap();
    *guard = None;
    drop(guard);
    unsafe { ffi::nwep_log_set_callback(None, std::ptr::null_mut()) }
}

/// `write` emits a log entry at the specified `level` through the C library's
/// logging subsystem.
///
/// The `trace_id` is a 16-byte distributed trace ID (pass `&[0u8; 16]` if no
/// trace context is available). The `component` string identifies the source of
/// the message (e.g., `"myapp::server"`). The `msg` string is the log message
/// text.
///
/// Internally the C library's `nwep_log_write` is called with a `"%s"` format
/// string to avoid interpreting any printf-style sequences in `msg`.
///
/// # Example
///
/// ```rust
/// use nwep::log::{write, LogLevel};
///
/// let tid = [0u8; 16];
/// write(LogLevel::Info, &tid, "myapp", "starting up");
/// ```
pub fn write(level: LogLevel, trace_id: &[u8; 16], component: &str, msg: &str) {
    let comp = CString::new(component).unwrap_or_default();
    let m = CString::new(msg).unwrap_or_default();
    let fmt = CString::new("%s").unwrap();
    unsafe {
        ffi::nwep_log_write(
            level.to_ffi(),
            trace_id.as_ptr(),
            comp.as_ptr(),
            fmt.as_ptr(),
            m.as_ptr(),
        )
    }
}

macro_rules! log_fn {
    ($name:ident, $ffi_fn:ident, $doc:expr) => {
        #[doc = $doc]
        pub fn $name(trace_id: &[u8; 16], component: &str, msg: &str) {
            let comp = CString::new(component).unwrap_or_default();
            let m = CString::new(msg).unwrap_or_default();
            let fmt = CString::new("%s").unwrap();
            unsafe { ffi::$ffi_fn(trace_id.as_ptr(), comp.as_ptr(), fmt.as_ptr(), m.as_ptr()) }
        }
    };
}

log_fn!(
    trace,
    nwep_log_trace,
    "`trace` emits a log entry at `LogLevel::Trace` level.

`trace_id` is a 16-byte distributed trace ID (use `&[0u8; 16]` when no trace
context is available). `component` identifies the source of the message and
`msg` is the log text. Internally the C library is called with a `\"%s\"` format
string so no printf-style sequences in `msg` are interpreted.

This function should only be called when the current level is
`LogLevel::Trace`; at higher levels the C library discards the entry before
any work is done.

# Example

```rust
use nwep::log::trace;
let tid = [0u8; 16];
trace(&tid, \"myapp\", \"entered request handler\");
```"
);

log_fn!(
    debug,
    nwep_log_debug,
    "`debug` emits a log entry at `LogLevel::Debug` level.

See `write` for a description of the parameters.

# Example

```rust
use nwep::log::debug;
let tid = [0u8; 16];
debug(&tid, \"myapp\", \"connection established\");
```"
);

log_fn!(
    info,
    nwep_log_info,
    "`info` emits a log entry at `LogLevel::Info` level.

See `write` for a description of the parameters.

# Example

```rust
use nwep::log::info;
let tid = [0u8; 16];
info(&tid, \"myapp\", \"server listening on :6937\");
```"
);

log_fn!(
    warn,
    nwep_log_warn,
    "`warn` emits a log entry at `LogLevel::Warn` level.

See `write` for a description of the parameters.

# Example

```rust
use nwep::log::warn;
let tid = [0u8; 16];
warn(&tid, \"myapp\", \"retrying after transient error\");
```"
);

log_fn!(
    error,
    nwep_log_error,
    "`error` emits a log entry at `LogLevel::Error` level.

See `write` for a description of the parameters.

# Example

```rust
use nwep::log::error;
let tid = [0u8; 16];
error(&tid, \"myapp\", \"fatal: failed to bind socket\");
```"
);