Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down
36 changes: 36 additions & 0 deletions USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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:
Expand Down
235 changes: 235 additions & 0 deletions src/config/mod.rs
Original file line number Diff line number Diff line change
@@ -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::<u16>() {
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<usize> {
if value.is_empty() {
return None;
}

if let Some(kb_str) = value.strip_suffix(['K', 'k']) {
kb_str
.parse::<usize>()
.ok()
.filter(|&kb| kb > 0)
.map(|kb| kb * 1024)
} else {
value.parse::<usize>().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<Config> = 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<F>(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);
}
}
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
pub mod api;
pub mod config;
pub mod services;
pub mod storage;
pub mod types;
21 changes: 18 additions & 3 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -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();
Expand Down
Loading