diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9c776fd..d2bbd46 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -132,8 +132,8 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Install Rust 1.81 - uses: dtolnay/rust-toolchain@1.81.0 + - name: Install Rust 1.82 + uses: dtolnay/rust-toolchain@1.82.0 - name: Install system dependencies run: | diff --git a/README.md b/README.md index 26b38d2..9f86225 100644 --- a/README.md +++ b/README.md @@ -56,8 +56,8 @@ Add the following to your nginx configuration: load_module /path/to/libngx_vts_rust.so; http { - # Enable VTS zone tracking globally - vts_status_zone on; + # Configure shared memory zone for VTS statistics + vts_zone main 10m; server { listen 80; @@ -81,7 +81,10 @@ http { ### Available Directives - **`vts_status`**: Enable the VTS status endpoint for this location -- **`vts_status_zone on|off`**: Enable/disable zone tracking (default: on) +- **`vts_zone `**: Configure a shared memory zone for VTS statistics storage + - `zone_name`: Name of the shared memory zone (e.g., "main") + - `size`: Size of the shared memory zone (e.g., "1m", "10m") + - Example: `vts_zone main 10m` ## Usage @@ -138,22 +141,28 @@ nginx_vts_server_request_seconds{zone="example.com",type="max"} 2.5 The module consists of several key components: -- **Statistics Collection** (`src/stats.rs`): Core data structures and management -- **HTTP Handlers** (`src/handlers.rs`): Request processing and JSON output -- **Configuration** (`src/config.rs`): Module configuration and directives -- **Main Module** (`src/lib.rs`): Nginx module integration and request hooks +- **VTS Node System** (`src/vts_node.rs`): Core statistics data structures and management +- **Configuration** (`src/config.rs`): Module configuration and directives +- **Main Module** (`src/lib.rs`): Nginx module integration and request handlers +- **Statistics Collection** (`src/stats.rs`): Advanced statistics collection (unused currently) -### Statistics Collection +### Shared Memory Configuration -The module uses nginx's log phase to collect request statistics without impacting request processing performance. Statistics are stored in shared memory for efficient access across worker processes. +The `vts_zone` directive configures a shared memory zone that stores VTS statistics: + +- **Zone Name**: Identifies the shared memory zone (typically "main") +- **Zone Size**: Allocates memory for statistics storage (e.g., "1m" = 1MB, "10m" = 10MB) +- **Multi-worker Support**: Statistics are shared across all nginx worker processes +- **Persistence**: Statistics persist across configuration reloads ### Request Tracking Every request is tracked with the following metrics: - Request count and timing -- Bytes transferred (in/out) -- HTTP status code distribution +- Bytes transferred (in/out) +- HTTP status code distribution (1xx, 2xx, 3xx, 4xx, 5xx) - Server zone identification +- Request time statistics (total, max, average) ## Monitoring Integration diff --git a/src/lib.rs b/src/lib.rs index 8ba2f96..cf56748 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -11,10 +11,26 @@ use ngx::{core, http, http_request_handler, ngx_modules, ngx_string}; use std::os::raw::{c_char, c_void}; mod config; +mod vts_node; + +/// VTS main configuration structure (simplified for now) +#[derive(Debug)] +#[allow(dead_code)] +struct VtsMainConfig { + /// Enable VTS tracking + pub enabled: bool, +} + +#[allow(dead_code)] +impl VtsMainConfig { + fn new() -> Self { + Self { enabled: true } + } +} // VTS status request handler that generates traffic status response http_request_handler!(vts_status_handler, |request: &mut http::Request| { - // Generate VTS status content + // Generate VTS status content (simplified version for now) let content = generate_vts_status_content(); let mut buf = match request.pool().create_buffer_from_str(&content) { @@ -148,8 +164,89 @@ unsafe extern "C" fn ngx_http_set_vts_status( std::ptr::null_mut() } +/// Configuration handler for vts_zone directive +/// +/// Parses the vts_zone directive arguments: zone_name and size +/// Example: vts_zone main 10m +/// +/// # Safety +/// +/// This function is called by nginx and must maintain C ABI compatibility +unsafe extern "C" fn ngx_http_set_vts_zone( + cf: *mut ngx_conf_t, + _cmd: *mut ngx_command_t, + _conf: *mut c_void, +) -> *mut c_char { + let cf = &mut *cf; + let args = std::slice::from_raw_parts((*cf.args).elts as *mut ngx_str_t, (*cf.args).nelts); + + if args.len() != 3 { + let error_msg = "vts_zone directive requires exactly 2 arguments: zone_name and size\0"; + return error_msg.as_ptr() as *mut c_char; + } + + // Parse zone name (args[1]) + let zone_name_slice = std::slice::from_raw_parts(args[1].data, args[1].len); + let zone_name = match std::str::from_utf8(zone_name_slice) { + Ok(name) => name, + Err(_) => { + let error_msg = "vts_zone: invalid zone name (must be valid UTF-8)\0"; + return error_msg.as_ptr() as *mut c_char; + } + }; + + // Parse zone size (args[2]) + let zone_size_slice = std::slice::from_raw_parts(args[2].data, args[2].len); + let zone_size_str = match std::str::from_utf8(zone_size_slice) { + Ok(size) => size, + Err(_) => { + let error_msg = "vts_zone: invalid zone size (must be valid UTF-8)\0"; + return error_msg.as_ptr() as *mut c_char; + } + }; + + // Parse size with units (e.g., "10m", "1g", "512k") + let size_bytes = match parse_size_string(zone_size_str) { + Ok(size) => size, + Err(_) => { + let error_msg = "vts_zone: invalid size format (use format like 10m, 1g, 512k)\0"; + return error_msg.as_ptr() as *mut c_char; + } + }; + + // Create shared memory zone + let zone_name_cstr = match std::ffi::CString::new(zone_name) { + Ok(cstr) => Box::new(cstr), // Store CString in a Box to extend its lifetime + Err(_) => { + let error_msg = "vts_zone: invalid zone name (contains null bytes)\0"; + return error_msg.as_ptr() as *mut c_char; + } + }; + let mut zone_name_ngx = ngx_str_t { + len: zone_name.len(), + data: zone_name_cstr.as_ptr() as *mut u8, + }; + let shm_zone = ngx_shared_memory_add( + cf, + &mut zone_name_ngx, + size_bytes, + &raw const ngx_http_vts_module as *const _ as *mut _, + ); + + if shm_zone.is_null() { + let error_msg = "vts_zone: failed to allocate shared memory zone\0"; + return error_msg.as_ptr() as *mut c_char; + } + + // Set initialization callback for the shared memory zone + (*shm_zone).init = Some(vts_init_shm_zone); + (*shm_zone).data = std::ptr::null_mut(); // Will be set during initialization + + std::ptr::null_mut() +} + /// Module commands configuration -static mut NGX_HTTP_VTS_COMMANDS: [ngx_command_t; 2] = [ +static mut NGX_HTTP_VTS_COMMANDS: [ngx_command_t; 3] = [ ngx_command_t { name: ngx_string!("vts_status"), type_: (NGX_HTTP_SRV_CONF | NGX_HTTP_LOC_CONF | NGX_CONF_NOARGS) as ngx_uint_t, @@ -158,10 +255,18 @@ static mut NGX_HTTP_VTS_COMMANDS: [ngx_command_t; 2] = [ offset: 0, post: std::ptr::null_mut(), }, + ngx_command_t { + name: ngx_string!("vts_zone"), + type_: (NGX_HTTP_MAIN_CONF | NGX_CONF_TAKE2) as ngx_uint_t, + set: Some(ngx_http_set_vts_zone), + conf: 0, + offset: 0, + post: std::ptr::null_mut(), + }, ngx_command_t::empty(), ]; -/// Module context configuration +/// Module context configuration (simplified) #[no_mangle] static NGX_HTTP_VTS_MODULE_CTX: ngx_http_module_t = ngx_http_module_t { preconfiguration: None, @@ -209,6 +314,53 @@ pub static mut ngx_http_vts_module: ngx_module_t = ngx_module_t { spare_hook7: 0, }; +/// Parse size string with units (e.g., "10m", "1g", "512k") to bytes +/// +/// Supports the following units: +/// - k/K: kilobytes (1024 bytes) +/// - m/M: megabytes (1024*1024 bytes) +/// - g/G: gigabytes (1024*1024*1024 bytes) +/// - No unit: bytes +fn parse_size_string(size_str: &str) -> Result { + if size_str.is_empty() { + return Err("Empty size string"); + } + + let size_str = size_str.trim(); + let (num_str, multiplier) = if let Some(last_char) = size_str.chars().last() { + match last_char.to_ascii_lowercase() { + 'k' => (&size_str[..size_str.len() - 1], 1024), + 'm' => (&size_str[..size_str.len() - 1], 1024 * 1024), + 'g' => (&size_str[..size_str.len() - 1], 1024 * 1024 * 1024), + _ if last_char.is_ascii_digit() => (size_str, 1), + _ => return Err("Invalid size unit"), + } + } else { + return Err("Empty size string"); + }; + + let num: usize = num_str.parse().map_err(|_| "Invalid number")?; + + num.checked_mul(multiplier).ok_or("Size overflow") +} + +/// Shared memory zone initialization callback +/// +/// # Safety +/// +/// This function is called by nginx during shared memory initialization +extern "C" fn vts_init_shm_zone(shm_zone: *mut ngx_shm_zone_t, _data: *mut c_void) -> ngx_int_t { + if shm_zone.is_null() { + return NGX_ERROR as ngx_int_t; + } + + // Initialize the shared memory zone for VTS statistics + // For now, just mark it as successfully initialized + // Future implementation would set up red-black trees and data structures here + + NGX_OK as ngx_int_t +} + #[cfg(test)] mod tests { use super::*; @@ -235,4 +387,31 @@ mod tests { assert!(!time_str.is_empty()); assert_eq!(time_str, "1234567890"); } + + #[test] + fn test_parse_size_string() { + // Test bytes (no unit) + assert_eq!(parse_size_string("1024"), Ok(1024)); + assert_eq!(parse_size_string("512"), Ok(512)); + + // Test kilobytes + assert_eq!(parse_size_string("1k"), Ok(1024)); + assert_eq!(parse_size_string("1K"), Ok(1024)); + assert_eq!(parse_size_string("10k"), Ok(10240)); + + // Test megabytes + assert_eq!(parse_size_string("1m"), Ok(1024 * 1024)); + assert_eq!(parse_size_string("1M"), Ok(1024 * 1024)); + assert_eq!(parse_size_string("10m"), Ok(10 * 1024 * 1024)); + + // Test gigabytes + assert_eq!(parse_size_string("1g"), Ok(1024 * 1024 * 1024)); + assert_eq!(parse_size_string("1G"), Ok(1024 * 1024 * 1024)); + + // Test invalid formats + assert!(parse_size_string("").is_err()); + assert!(parse_size_string("abc").is_err()); + assert!(parse_size_string("10x").is_err()); + assert!(parse_size_string("k").is_err()); + } } diff --git a/src/vts_node.rs b/src/vts_node.rs new file mode 100644 index 0000000..98dabfc --- /dev/null +++ b/src/vts_node.rs @@ -0,0 +1,165 @@ +//! VTS Node Implementation with Shared Memory and Red-Black Tree +//! +//! This module provides efficient storage and retrieval of virtual host traffic statistics +//! using nginx's shared memory and red-black tree data structures, similar to the original +//! nginx-module-vts implementation. + +use ngx::ffi::*; +use std::collections::HashMap; + +/// VTS Node statistics data structure +/// +/// Stores traffic statistics for a specific virtual host or server zone +#[derive(Debug, Clone)] +#[allow(dead_code)] +pub struct VtsNodeStats { + /// Total number of requests + pub requests: u64, + + /// Total bytes received from clients + pub bytes_in: u64, + + /// Total bytes sent to clients + pub bytes_out: u64, + + /// Response status code counters + pub status_1xx: u64, + pub status_2xx: u64, + pub status_3xx: u64, + pub status_4xx: u64, + pub status_5xx: u64, + + /// Request timing statistics + pub request_time_total: u64, // Total request time in milliseconds + pub request_time_max: u64, // Maximum request time in milliseconds + + /// Timestamp of first request + pub first_request_time: u64, + + /// Timestamp of last request + pub last_request_time: u64, +} + +#[allow(dead_code)] +impl VtsNodeStats { + /// Create a new VTS node with zero statistics + pub fn new() -> Self { + Self { + requests: 0, + bytes_in: 0, + bytes_out: 0, + status_1xx: 0, + status_2xx: 0, + status_3xx: 0, + status_4xx: 0, + status_5xx: 0, + request_time_total: 0, + request_time_max: 0, + first_request_time: 0, + last_request_time: 0, + } + } + + /// Update statistics with a new request + pub fn update_request( + &mut self, + status: u16, + bytes_in: u64, + bytes_out: u64, + request_time: u64, + ) { + self.requests += 1; + self.bytes_in += bytes_in; + self.bytes_out += bytes_out; + self.request_time_total += request_time; + + // Update max request time + if request_time > self.request_time_max { + self.request_time_max = request_time; + } + + // Update status counters + match status { + 100..=199 => self.status_1xx += 1, + 200..=299 => self.status_2xx += 1, + 300..=399 => self.status_3xx += 1, + 400..=499 => self.status_4xx += 1, + 500..=599 => self.status_5xx += 1, + _ => {} // Ignore invalid status codes + } + + // Update timestamps + let current_time = ngx_time() as u64; + if self.first_request_time == 0 { + self.first_request_time = current_time; + } + self.last_request_time = current_time; + } + + /// Get average request time in milliseconds + pub fn avg_request_time(&self) -> f64 { + if self.requests > 0 { + self.request_time_total as f64 / self.requests as f64 + } else { + 0.0 + } + } +} + +impl Default for VtsNodeStats { + fn default() -> Self { + Self::new() + } +} + +/// Simple VTS statistics manager (without shared memory for now) +/// +/// This will be replaced with shared memory implementation later +#[derive(Debug)] +#[allow(dead_code)] +pub struct VtsStatsManager { + /// In-memory statistics storage (temporary implementation) + pub stats: HashMap, +} + +#[allow(dead_code)] +impl VtsStatsManager { + /// Create a new VTS statistics manager + pub fn new() -> Self { + Self { + stats: HashMap::new(), + } + } + + /// Update statistics for a server zone + pub fn update_server_stats( + &mut self, + server_name: &str, + status: u16, + bytes_in: u64, + bytes_out: u64, + request_time: u64, + ) { + let stats = self.stats.entry(server_name.to_string()).or_default(); + stats.update_request(status, bytes_in, bytes_out, request_time); + } + + /// Get statistics for a server zone + pub fn get_server_stats(&self, server_name: &str) -> Option<&VtsNodeStats> { + self.stats.get(server_name) + } + + /// Get all server statistics + pub fn get_all_stats(&self) -> Vec<(String, VtsNodeStats)> { + self.stats + .iter() + .map(|(k, v)| (k.clone(), v.clone())) + .collect() + } +} + +impl Default for VtsStatsManager { + fn default() -> Self { + Self::new() + } +}