diff --git a/Dockerfile b/Dockerfile index 216b587..222fdf7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -42,12 +42,17 @@ RUN chown tlq:tlq /usr/local/bin/tlq && chmod +x /usr/local/bin/tlq # Switch to non-root user USER tlq -# Expose port +# Set default configuration via environment variables +ENV TLQ_PORT=1337 +ENV TLQ_MAX_MESSAGE_SIZE=65536 +ENV TLQ_LOG_LEVEL=info + +# Expose port (note: if TLQ_PORT is changed, map ports accordingly) EXPOSE 1337 # Health check HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ - CMD curl -f http://localhost:1337/hello || exit 1 + CMD ["/bin/sh", "-c", "curl -f http://localhost:${TLQ_PORT:-1337}/hello || exit 1"] # Run the binary CMD ["tlq"] \ No newline at end of file diff --git a/README.md b/README.md index 9a3ef5d..90f97be 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,10 @@ cargo install tlq # Using Docker docker run -p 1337:1337 nebojsa/tlq + +# Docker with custom configuration +docker run -e TLQ_PORT=8080 -p 8080:8080 nebojsa/tlq +docker run -e TLQ_MAX_MESSAGE_SIZE=1048576 -e TLQ_LOG_LEVEL=debug -p 1337:1337 nebojsa/tlq ``` ### Use @@ -51,6 +55,20 @@ curl -X POST localhost:1337/retry \ - **Auto-locking** - Messages lock on retrieval - **Client libraries** - [Rust](https://crates.io/crates/tlq-client), [Node.js](https://www.npmjs.com/package/tlq-client), [Python](https://pypi.org/project/tlq-client/), [Go](https://pkg.go.dev/github.com/skyaktech/tlq-client-go) +## Configuration + +You can configure TLQ via environment variables (all optional; defaults shown): +- TLQ_PORT: TCP port to listen on. Default: 1337 +- TLQ_MAX_MESSAGE_SIZE: Maximum message body size in bytes. Default: 65536 +- TLQ_LOG_LEVEL: Log verbosity (trace, debug, info, warn, error). Default: info + +Examples: + +```bash +TLQ_PORT=8080 tlq +TLQ_MAX_MESSAGE_SIZE=1048576 TLQ_LOG_LEVEL=debug tlq +``` + ## Why TLQ? Perfect for: diff --git a/USAGE.md b/USAGE.md index b7ecfff..54e103f 100644 --- a/USAGE.md +++ b/USAGE.md @@ -34,7 +34,20 @@ If the command is not found, you can run it directly with: Run TLQ using the official Docker image: ```bash +# Default configuration docker run -p 1337:1337 nebojsa/tlq + +# Custom port (note: port mapping must match TLQ_PORT) +docker run -e TLQ_PORT=8080 -p 8080:8080 nebojsa/tlq + +# Custom message size limit (using k suffix) +docker run -e TLQ_MAX_MESSAGE_SIZE=128k -p 1337:1337 nebojsa/tlq + +# Debug logging +docker run -e TLQ_LOG_LEVEL=debug -p 1337:1337 nebojsa/tlq + +# Multiple options combined +docker run -e TLQ_PORT=9000 -e TLQ_LOG_LEVEL=debug -p 9000:9000 nebojsa/tlq ``` ### Building from Source @@ -61,6 +74,29 @@ curl http://localhost:1337/hello # Returns: "Hello World" ``` +## Configuration + +TLQ can be configured via environment variables. All are optional; defaults are shown. + +- TLQ_PORT: TCP port to listen on. Default: 1337 +- TLQ_MAX_MESSAGE_SIZE: Maximum message body size in bytes. Supports K/k suffix (e.g., 128K = 131072 bytes). Default: 65536 +- TLQ_LOG_LEVEL: Log verbosity (trace, debug, info, warn, error). Default: info + +Examples: + +```bash +# Change port +TLQ_PORT=8080 tlq + +# Increase message size to 1MB and use debug logs +TLQ_MAX_MESSAGE_SIZE=128k TLQ_LOG_LEVEL=debug tlq + +# Alternative: specify size in bytes +TLQ_MAX_MESSAGE_SIZE=32768 TLQ_LOG_LEVEL=debug tlq +``` + +Note: The official Dockerfile exposes and health-checks port 1337 by default; if you change TLQ_PORT inside the container, you may want to adjust your run command and health checks accordingly. + ## Client Libraries Official clients are available for: diff --git a/src/config/mod.rs b/src/config/mod.rs new file mode 100644 index 0000000..114606d --- /dev/null +++ b/src/config/mod.rs @@ -0,0 +1,235 @@ +use std::env; +use std::sync::OnceLock; +use tracing::Level; + +const DEFAULT_PORT: u16 = 1337; +const DEFAULT_MAX_MESSAGE_SIZE: usize = 65536; // 64KB +const DEFAULT_LOG_LEVEL: &str = "info"; + +#[derive(Debug, Clone)] +pub struct Config { + pub port: u16, + pub max_message_size: usize, + pub log_level: String, +} + +impl Default for Config { + fn default() -> Self { + Self { + port: DEFAULT_PORT, + max_message_size: DEFAULT_MAX_MESSAGE_SIZE, + log_level: DEFAULT_LOG_LEVEL.to_string(), + } + } +} + +impl Config { + pub fn from_env() -> Self { + let mut config = Config::default(); + + if let Ok(env_value) = env::var("TLQ_PORT") { + if let Ok(port) = env_value.parse::() { + config.port = port; + } + } + + if let Ok(env_value) = env::var("TLQ_MAX_MESSAGE_SIZE") { + if let Some(size) = Self::parse_size(&env_value) { + config.max_message_size = size; + } + } + + if let Ok(v) = env::var("TLQ_LOG_LEVEL") { + config.log_level = v; + } + + config + } + + fn parse_size(value: &str) -> Option { + if value.is_empty() { + return None; + } + + if let Some(kb_str) = value.strip_suffix(['K', 'k']) { + kb_str + .parse::() + .ok() + .filter(|&kb| kb > 0) + .map(|kb| kb * 1024) + } else { + value.parse::().ok().filter(|&bytes| bytes > 0) + } + } + + pub fn tracing_level(&self) -> Level { + match self.log_level.to_lowercase().as_str() { + "trace" => Level::TRACE, + "debug" => Level::DEBUG, + "info" => Level::INFO, + "warn" | "warning" => Level::WARN, + "error" => Level::ERROR, + _ => Level::INFO, + } + } +} + +static CONFIG: OnceLock = OnceLock::new(); + +pub fn config() -> &'static Config { + CONFIG.get_or_init(Config::from_env) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::env; + use std::sync::Mutex; + + // Ensure tests don't run in parallel and interfere with each other's env vars + static TEST_MUTEX: Mutex<()> = Mutex::new(()); + + fn with_env_var(key: &str, value: &str, test: F) + where + F: FnOnce(), + { + let _lock = TEST_MUTEX.lock().unwrap(); + clear_env_vars(); + env::set_var(key, value); + test(); + clear_env_vars(); + } + + fn clear_env_vars() { + env::remove_var("TLQ_PORT"); + env::remove_var("TLQ_MAX_MESSAGE_SIZE"); + env::remove_var("TLQ_LOG_LEVEL"); + } + + #[test] + fn test_default_config() { + let _lock = TEST_MUTEX.lock().unwrap(); + clear_env_vars(); + let config = Config::from_env(); + assert_eq!(config.port, DEFAULT_PORT); + assert_eq!(config.max_message_size, DEFAULT_MAX_MESSAGE_SIZE); + assert_eq!(config.log_level, DEFAULT_LOG_LEVEL); + } + + #[test] + fn test_ports() { + let test_cases = vec![ + ("8080", 8080, "valid port"), + ("not-a-port", DEFAULT_PORT, "invalid string"), + ("99999", DEFAULT_PORT, "out of range"), + ("", DEFAULT_PORT, "empty string"), + ]; + + for (input, expected_port, description) in test_cases { + with_env_var("TLQ_PORT", input, || { + let config = Config::from_env(); + assert_eq!( + config.port, expected_port, + "Failed for {}: input '{}'", + description, input + ); + }); + } + } + + #[test] + fn test_message_sizes() { + let test_cases = vec![ + ("1024", 1024, "raw bytes"), + ("64K", 64 * 1024, "uppercase K suffix"), + ("128k", 128 * 1024, "lowercase k suffix"), + ("abc", DEFAULT_MAX_MESSAGE_SIZE, "invalid format"), + ("K", DEFAULT_MAX_MESSAGE_SIZE, "just K"), + ("0", DEFAULT_MAX_MESSAGE_SIZE, "zero value"), + ("0k", DEFAULT_MAX_MESSAGE_SIZE, "zero with k suffix"), + ("", DEFAULT_MAX_MESSAGE_SIZE, "empty string"), + ]; + + for (input, expected_size, description) in test_cases { + with_env_var("TLQ_MAX_MESSAGE_SIZE", input, || { + let config = Config::from_env(); + assert_eq!( + config.max_message_size, expected_size, + "Failed for {}: input '{}'", + description, input + ); + }); + } + } + #[test] + fn test_log_levels() { + let test_cases = vec![ + ("trace", Level::TRACE), + ("debug", Level::DEBUG), + ("info", Level::INFO), + ("warn", Level::WARN), + ("warning", Level::WARN), + ("error", Level::ERROR), + ("INFO", Level::INFO), + ("Info", Level::INFO), + ("invalid", Level::INFO), + ("", Level::INFO), + ]; + + for (input, expected_level) in test_cases { + with_env_var("TLQ_LOG_LEVEL", input, || { + let config = Config::from_env(); + assert_eq!(config.log_level, input); + assert_eq!( + config.tracing_level(), + expected_level, + "Failed for log level: {}", + input + ); + }); + } + } + + #[test] + fn test_multiple_env_vars() { + let _lock = TEST_MUTEX.lock().unwrap(); + clear_env_vars(); + env::set_var("TLQ_PORT", "3000"); + env::set_var("TLQ_MAX_MESSAGE_SIZE", "32K"); + env::set_var("TLQ_LOG_LEVEL", "debug"); + + let config = Config::from_env(); + assert_eq!(config.port, 3000); + assert_eq!(config.max_message_size, 32 * 1024); + assert_eq!(config.log_level, "debug"); + + clear_env_vars(); + } + + #[test] + fn test_partial_env_vars() { + with_env_var("TLQ_PORT", "5000", || { + let config = Config::from_env(); + assert_eq!(config.port, 5000); + assert_eq!(config.max_message_size, DEFAULT_MAX_MESSAGE_SIZE); + assert_eq!(config.log_level, DEFAULT_LOG_LEVEL); + }); + } + + #[test] + fn test_parse_size_helper() { + // Valid cases + assert_eq!(Config::parse_size("1024"), Some(1024)); + assert_eq!(Config::parse_size("64K"), Some(65536)); + assert_eq!(Config::parse_size("64k"), Some(65536)); + assert_eq!(Config::parse_size("1K"), Some(1024)); + + // Invalid cases + assert_eq!(Config::parse_size(""), None); + assert_eq!(Config::parse_size("0"), None); + assert_eq!(Config::parse_size("0K"), None); + assert_eq!(Config::parse_size("K"), None); + assert_eq!(Config::parse_size("abc"), None); + assert_eq!(Config::parse_size("-1"), None); + } +} diff --git a/src/lib.rs b/src/lib.rs index 718e143..77bbd3d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,5 @@ pub mod api; +pub mod config; pub mod services; pub mod storage; pub mod types; diff --git a/src/main.rs b/src/main.rs index 9be50ed..2af747d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,21 +1,36 @@ use std::sync::Arc; use tlq::api::create_api; +use tlq::config::config; use tlq::services::MessageService; use tlq::storage::memory::MemoryStorage; use tracing::info; -use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; +use tracing_subscriber::{ + filter::LevelFilter, layer::Layer, layer::SubscriberExt, util::SubscriberInitExt, +}; #[tokio::main] async fn main() { + let cfg = config(); + tracing_subscriber::registry() - .with(tracing_subscriber::fmt::layer()) + .with( + tracing_subscriber::fmt::layer() + .with_filter(LevelFilter::from_level(cfg.tracing_level())), + ) .init(); + info!( + "Starting TLQ with configuration: port={}, max_message_size={}, log_level={}", + cfg.port, cfg.max_message_size, cfg.log_level + ); + let store = Arc::new(MemoryStorage::new()); let service = MessageService::new(store); let app = create_api(service); - let listener = tokio::net::TcpListener::bind("0.0.0.0:1337").await.unwrap(); + let bind_addr = format!("0.0.0.0:{}", cfg.port); + let listener = tokio::net::TcpListener::bind(bind_addr).await.unwrap(); + info!("Listening on {}", listener.local_addr().unwrap()); axum::serve(listener, app).await.unwrap(); diff --git a/src/services/mod.rs b/src/services/mod.rs index e92238a..48fdbcd 100644 --- a/src/services/mod.rs +++ b/src/services/mod.rs @@ -1,3 +1,4 @@ +use crate::config; use crate::storage::traits::Storage; use crate::types::Message; use std::sync::Arc; @@ -13,11 +14,9 @@ impl MessageService { } } -const MESSAGE_SIZE_LIMIT: usize = 65536; // 64KB - impl MessageService { pub async fn add(&self, body: String) -> Result { - if body.len() > MESSAGE_SIZE_LIMIT { + if body.len() > config::config().max_message_size { return Err("Message body size is too large".to_string()); } @@ -78,7 +77,7 @@ mod tests { let store = Arc::new(MemoryStorage::new()); let service = MessageService::new(store); - let body = "A".repeat(MESSAGE_SIZE_LIMIT); + let body = "A".repeat(config::config().max_message_size); let result = service.add(body).await; assert!(result.is_ok()); } @@ -88,7 +87,7 @@ mod tests { let store = Arc::new(MemoryStorage::new()); let service = MessageService::new(store); - let body = "A".repeat(MESSAGE_SIZE_LIMIT + 1); + let body = "A".repeat(config::config().max_message_size + 1); let result = service.add(body).await; assert!(result.is_err()); assert_eq!(result.unwrap_err(), "Message body size is too large");