ripress 2.5.1

An Express.js-inspired web framework for Rust
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
#![warn(missing_docs)]
use crate::{context::HttpResponse, next::Next, req::HttpRequest, types::MiddlewareOutput};
use std::collections::HashMap;
use tracing::info;

/// Builtin Logger Middleware
///
/// This middleware provides comprehensive request and response logging capabilities
/// for HTTP requests. It captures various aspects of each request including method,
/// path, status code, headers, IP address, user agent, query parameters, and body size.
/// The middleware is highly configurable, allowing you to selectively enable/disable
/// different logging components and exclude specific paths from logging.
///
/// ## Features
///
/// * **Selective logging** - Enable/disable individual log components
/// * **Custom header logging** - Log specific headers by name
/// * **Path exclusion** - Skip logging for specified paths (e.g., health checks)
/// * **Query parameter logging** - Capture and log request query parameters
/// * **Body size tracking** - Log response body size with stream detection
/// * **IP address logging** - Capture client IP addresses
/// * **User agent detection** - Log client user agent strings
/// * **Prefix-based exclusion** - Exclude paths using prefix matching
/// * **Thread-safe operation** - Safe for concurrent use across multiple threads
/// * **Zero-allocation exclusion** - Excluded requests bypass all processing
///
/// ## Configuration
///
/// The middleware accepts an optional `LoggerConfig` struct to customize logging behavior:
///
/// * `method` - Log HTTP method (GET, POST, etc.) - default: true
/// * `path` - Log request path - default: true
/// * `status` - Log response status code - default: true
/// * `user_agent` - Log client user agent - default: true
/// * `ip` - Log client IP address - default: true
/// * `headers` - List of specific headers to log - default: empty
/// * `body_size` - Log response body size - default: true
/// * `query_params` - Log query parameters - default: true
/// * `exclude_paths` - List of path prefixes to exclude from logging - default: empty
///
/// ## Path Exclusion Behavior
///
/// The `exclude_paths` configuration uses prefix matching for efficient filtering:
/// * `/health` excludes `/health`, `/health/live`, `/health/ready`, etc.
/// * `/api/internal` excludes all paths starting with `/api/internal`
/// * Excluded requests are processed with minimal overhead
/// * No log output is generated for excluded paths
///
/// ## Log Format
///
/// The middleware outputs structured logs using the `tracing` crate with the following format:
/// ```md
/// path: /api/users,
/// user_agent: Mozilla/5.0...,
/// ip: 192.168.1.1,
/// custom-header: value,
/// status_code: 200
/// query_params: {"id": "123", "format": "json"},
/// method: GET,
/// body_size: 1024
/// ```
///
/// ## Examples
///
/// Basic usage with default configuration:
///
/// ```no_run
/// use ripress::{app::App, middlewares::logger::LoggerConfig};
///
/// tracing_subscriber::fmt::init();
/// let mut app = App::new();
/// app.use_logger(Some(LoggerConfig::default()));
/// ```
///
/// Minimal logging configuration:
///
/// ```no_run
/// use ripress::{app::App, middlewares::logger::LoggerConfig};
/// tracing_subscriber::fmt::init();
///
/// let mut app = App::new();
/// let config = LoggerConfig {
///     method: true,
///     path: true,
///     status: true,
///     user_agent: false,
///     ip: false,
///     headers: vec![],
///     body_size: false,
///     query_params: false,
///     exclude_paths: vec![],
/// };
/// app.use_logger(Some(config));
/// ```
///
/// Custom header logging with path exclusions:
///
/// ```no_run
/// use ripress::{app::App, middlewares::logger::LoggerConfig};
/// tracing_subscriber::fmt::init();
///
/// let mut app = App::new();
/// let config = LoggerConfig {
///     method: true,
///     path: true,
///     status: true,
///     user_agent: true,
///     ip: true,
///     headers: vec![
///         "authorization".to_string(),
///         "x-request-id".to_string(),
///         "content-type".to_string(),
///     ],
///     body_size: true,
///     query_params: true,
///     exclude_paths: vec![
///         "/health".to_string(),
///         "/metrics".to_string(),
///         "/favicon.ico".to_string(),
///     ],
/// };
/// app.use_logger(Some(config));
/// ```
///
/// Using default configuration (recommended for development):
///
/// ```no_run
/// use ripress::app::App;
///
/// tracing_subscriber::fmt::init();
///
/// let mut app = App::new();
/// app.use_logger(None); // Uses LoggerConfig::default()
/// ```
///
/// Production configuration with security considerations:
///
/// ```no_run
/// use ripress::{app::App, middlewares::logger::LoggerConfig};
///
/// tracing_subscriber::fmt::init();
///
/// let mut app = App::new();
/// let config = LoggerConfig {
///     method: true,
///     path: true,
///     status: true,
///     user_agent: false, // May contain sensitive info
///     ip: true,
///     headers: vec![
///         "x-request-id".to_string(), // Safe to log
///         "content-length".to_string(),
///         // Note: Don't log authorization, cookies, or other sensitive headers
///     ],
///     body_size: true,
///     query_params: false, // May contain sensitive data
///     exclude_paths: vec![
///         "/health".to_string(),
///         "/metrics".to_string(),
///         "/internal".to_string(),
///     ],
/// };
/// app.use_logger(Some(config));
/// ```
///
/// ## Output Examples
///
/// Default configuration output:
/// ```md
/// path: /api/users/123,
/// user_agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36,
/// ip: 192.168.1.100,
/// status_code: 200
/// query_params: {"include": "profile", "format": "json"},
/// method: GET,
/// body_size: 2048
/// ```
///
/// Custom headers output:
/// ```md
/// path: /api/upload,
/// user_agent: PostmanRuntime/7.32.3,
/// ip: 10.0.0.15,
/// authorization: <missing>,
/// x-request-id: abc-123-def,
/// content-type: multipart/form-data,
/// status_code: 201
/// query_params: {},
/// method: POST,
/// body_size: stream
/// ```
///
/// ## Performance Considerations
///
/// * **Excluded paths** bypass all processing for maximum efficiency
/// * **Header lookups** are performed only for configured headers
/// * **String allocation** occurs only for enabled log components
/// * **Query parameter cloning** happens only when enabled
/// * **Arc-based config sharing** minimizes memory overhead
/// * **Structured output** reduces parsing overhead in log aggregators
///
/// ## Security Considerations
///
/// * **Sensitive headers** (Authorization, Cookie, etc.) should not be logged
/// * **Query parameters** may contain sensitive data - consider disabling
/// * **User agents** may contain personally identifiable information
/// * **IP addresses** may be subject to privacy regulations (GDPR, etc.)
/// * **Path exclusions** prevent accidental logging of sensitive endpoints
///
/// ## Integration with Log Aggregators
///
/// The structured output format is designed for easy parsing by log aggregation systems:
/// * **Consistent field names** for reliable parsing
/// * **Comma-separated format** for simple splitting
/// * **Missing header indicator** (`<missing>`) for clear status
/// * **Stream detection** for body size handling
/// * **JSON-formatted query params** for structured data
///
/// ## Troubleshooting
///
/// Common issues and solutions:
/// * **Missing headers**: Check header name case (middleware converts to lowercase)
/// * **No output**: Verify path isn't in `exclude_paths`
/// * **Partial logs**: Check individual boolean flags in configuration
/// * **Performance impact**: Use path exclusions for high-traffic endpoints
///
/// ## Thread Safety
///
/// The middleware is fully thread-safe:
/// * Configuration is wrapped in `Arc` for efficient sharing
/// * No mutable state is maintained between requests
/// * Safe for use in multi-threaded web servers
/// * Clone-friendly for multiple route registration

/// Configuration struct for the Logger Middleware
///
/// This struct controls which aspects of HTTP requests and responses are logged.
/// All fields are boolean flags or collections that determine what information
/// is captured and output for each request.
///
/// ## Field Details
///
/// * `method` - Logs the HTTP method (GET, POST, PUT, DELETE, etc.)
/// * `path` - Logs the request path (e.g., "/api/users/123")
/// * `status` - Logs the HTTP response status code (200, 404, 500, etc.)
/// * `user_agent` - Logs the User-Agent header sent by the client
/// * `ip` - Logs the client's IP address
/// * `headers` - A list of specific header names to log (case-insensitive)
/// * `body_size` - Logs the size of the response body in bytes, or "stream" for streaming responses
/// * `query_params` - Logs URL query parameters as a structured format
/// * `exclude_paths` - Path prefixes that should be excluded from logging entirely
///
/// ## Default Configuration
///
/// By default, all standard fields are enabled and no paths are excluded:
/// - All boolean fields default to `true`
/// - `headers` defaults to empty (no custom headers logged)
/// - `exclude_paths` defaults to empty (all paths logged)
#[derive(Clone)]
pub struct LoggerConfig {
    /// Whether to log the HTTP method (GET, POST, etc.)
    pub method: bool,
    /// Whether to log the request path
    pub path: bool,
    /// Whether to log the response status code
    pub status: bool,
    /// Whether to log the client's User-Agent header
    pub user_agent: bool,
    /// Whether to log the client's IP address
    pub ip: bool,
    /// List of specific header names to log (converted to lowercase)
    ///
    /// Headers not present in the request will show as "<missing>" in the log output.
    /// Common headers to log: "content-type", "x-request-id", "authorization"
    pub headers: Vec<String>,
    /// Whether to log the response body size
    ///
    /// Shows actual byte count for regular responses, "stream" for streaming responses.
    pub body_size: bool,
    /// Whether to log URL query parameters
    ///
    /// Query parameters are logged in a structured JSON-like format for easy parsing.
    pub query_params: bool,
    /// List of path prefixes to exclude from logging
    ///
    /// Uses prefix matching: "/health" excludes "/health", "/health/live", etc.
    /// Useful for excluding health checks, metrics endpoints, and other high-frequency requests.
    pub exclude_paths: Vec<String>,
}

impl Default for LoggerConfig {
    fn default() -> Self {
        LoggerConfig {
            method: true,
            path: true,
            status: true,
            user_agent: true,
            ip: true,
            headers: vec![],
            body_size: true,
            query_params: true,
            exclude_paths: vec![],
        }
    }
}

/// Creates a logger middleware function
///
/// Returns a middleware function that logs HTTP request and response information
/// according to the provided configuration. The middleware processes requests
/// efficiently, with excluded paths bypassing all logging overhead.
///
/// ## Parameters
///
/// * `config` - Optional logging configuration. If `None`, uses `LoggerConfig::default()`
///   which enables all standard logging fields.
///
/// ## Returns
///
/// A middleware function compatible with the ripress framework that:
/// * Logs request and response information to stdout
/// * Skips processing for paths matching `exclude_paths` prefixes
/// * Captures configured headers with case-insensitive matching
/// * Handles streaming responses appropriately
/// * Operates efficiently with minimal allocation overhead
///
/// ## Thread Safety
///
/// The returned middleware is `Send + Sync` and safe for concurrent use.
/// Configuration is shared via `Arc` to minimize memory overhead when
/// the middleware is used across multiple routes or threads.
///
/// ## Performance Notes
///
/// * Excluded paths are checked first and bypass all other processing
/// * Header lookups only occur for headers specified in the configuration
/// * String formatting only happens for enabled log components
/// * Configuration is shared via Arc to avoid cloning overhead
///
/// ## Log Output
///
/// All log output is written using the `tracing` crate at the `info` level. The format is
/// comma-separated key-value pairs, making it suitable for structured
/// log parsing systems. Fields are output in a consistent order regardless
/// of configuration.
///
/// ## Tracing Setup
///
/// To see the log output, you need to initialize a tracing subscriber in your application:
///
/// ```rust
/// use tracing_subscriber;
///
/// fn main() {
///     // Initialize tracing subscriber
///     tracing_subscriber::fmt::init();
///     
///     // Your app code here
/// }
/// ```
pub(crate) fn logger(
    config: Option<LoggerConfig>,
) -> impl Fn(HttpRequest, HttpResponse, Next) -> MiddlewareOutput + Send + Sync + 'static {
    let cfg = std::sync::Arc::new(config.unwrap_or_default());
    move |req: HttpRequest, res, next| {
        let config = std::sync::Arc::clone(&cfg);

        if config
            .exclude_paths
            .iter()
            .any(|prefix| req.path.starts_with(prefix))
        {
            return Box::pin(async move {
                return next.call(req, res).await;
            });
        }

        Box::pin(async move {
            let path = req.path.clone();
            let method = req.method.clone();
            let user_agent = req.headers.user_agent().unwrap_or("Unknown").to_string();
            let ip = req.ip();
            let status_code = res.status_code;
            let mut headers = HashMap::new();

            if !config.headers.is_empty() {
                for header in &config.headers {
                    let key = header.to_ascii_lowercase();
                    let value = req
                        .headers
                        .get(&key)
                        .map(|v| v.to_string())
                        .unwrap_or_else(|| "<missing>".to_string());
                    headers.insert(key, value);
                }
            }

            let query_params = req.query.clone();

            let mut msg = String::new();

            if config.path {
                msg.push_str(&format!("path: {}, \n", path));
            }
            if config.user_agent {
                msg.push_str(&format!("user_agent: {}, \n", user_agent));
            }
            if config.ip {
                msg.push_str(&format!("ip: {}, \n", ip));
            }
            for (key, value) in headers {
                msg.push_str(&format!("{}: {}, \n", key, value));
            }
            if config.status {
                msg.push_str(&format!("status_code: {}\n", status_code));
            }
            if config.query_params {
                msg.push_str(&format!("query_params: {:?}, \n", query_params));
            }
            if config.method {
                msg.push_str(&format!("method: {}, \n", method));
            }
            if config.body_size {
                if res.stream.is_some() {
                    msg.push_str("body_size: stream\n");
                } else {
                    msg.push_str(&format!("body_size: {}\n", res.body.len()));
                }
            }

            let msg = msg.trim_end_matches([',', ' ', '\t', '\n']);

            info!("{}", msg);

            return next.call(req, res).await;
        })
    }
}