Skip to content

Commit b3952a8

Browse files
committed
Improve logging and health checks
1 parent 2e93d40 commit b3952a8

File tree

4 files changed

+67
-15
lines changed

4 files changed

+67
-15
lines changed

README.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ A lightweight, high-performance HTTP reverse proxy written in Rust, prioritizing
55
## Features
66

77
- **High Performance**: Built with Tokio and Hyper for maximum throughput
8-
- **Load Balancing**: Round-robin load balancing across multiple backend servers
8+
- **Load Balancing**: Round-robin and weighted round-robin across healthy backends
99
- **Health Checks**: Automatic health monitoring of backend servers
1010
- **Configuration-Driven**: YAML-based configuration for easy management
1111
- **Structured Logging**: Comprehensive request/response logging with configurable levels
@@ -58,7 +58,7 @@ backends:
5858
health_check_path: "/"
5959

6060
load_balancing:
61-
strategy: "round_robin" # Load balancing strategy
61+
strategy: "round_robin" # round_robin or weighted_round_robin
6262

6363
health_checks:
6464
enabled: true # Enable/disable health checks
@@ -86,10 +86,10 @@ The proxy consists of several key components:
8686
8787
## Load Balancing
8888
89-
Currently supports round-robin load balancing:
90-
- Requests are distributed evenly across healthy backends
91-
- Unhealthy backends are automatically excluded
92-
- Backends are marked unhealthy after consecutive failures
89+
Supports both round-robin and weighted round-robin strategies:
90+
- Round-robin distributes requests evenly across healthy backends
91+
- Weighted round-robin honors configured weights for skewed traffic
92+
- Unhealthy backends are automatically excluded until they recover
9393
9494
## Health Checks
9595

config.example.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ backends:
1212
health_check_path: "/posts/1"
1313

1414
load_balancing:
15-
strategy: "round_robin"
15+
strategy: "round_robin" # round_robin or weighted_round_robin
1616

1717
health_checks:
1818
enabled: true

config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ backends:
1111
health_check_path: "/"
1212

1313
load_balancing:
14-
strategy: "round_robin" # round_robin, weighted_round_robin, least_connections
14+
strategy: "round_robin" # round_robin or weighted_round_robin
1515

1616
health_checks:
1717
enabled: true

src/main.rs

Lines changed: 59 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ use hyper_util::rt::TokioIo;
1010
use log::{debug, error, info, warn};
1111
use serde::Deserialize;
1212
use std::collections::HashMap;
13+
use std::io::Write;
1314
use std::sync::atomic::{AtomicUsize, Ordering};
1415
use std::sync::Arc;
1516
use std::time::{Duration, Instant};
@@ -107,6 +108,12 @@ struct LoadBalancer {
107108
http_client: HttpClient,
108109
}
109110

111+
#[derive(Debug)]
112+
enum HealthProbeError {
113+
Timeout,
114+
Transport(String),
115+
}
116+
110117
impl LoadBalancer {
111118
fn new(
112119
backends: Vec<Backend>,
@@ -215,7 +222,26 @@ impl LoadBalancer {
215222
}
216223
};
217224

218-
match self.http_client.get(uri).await {
225+
let request_future = self.http_client.get(uri);
226+
let response_result = if config.timeout_seconds > 0 {
227+
match tokio::time::timeout(
228+
Duration::from_secs(config.timeout_seconds),
229+
request_future,
230+
)
231+
.await
232+
{
233+
Ok(inner_result) => {
234+
inner_result.map_err(|err| HealthProbeError::Transport(err.to_string()))
235+
}
236+
Err(_) => Err(HealthProbeError::Timeout),
237+
}
238+
} else {
239+
request_future
240+
.await
241+
.map_err(|err| HealthProbeError::Transport(err.to_string()))
242+
};
243+
244+
match response_result {
219245
Ok(response) => {
220246
let status = response.status();
221247
let mut health = self.health_status.write().await;
@@ -240,8 +266,19 @@ impl LoadBalancer {
240266
}
241267
}
242268
}
243-
Err(e) => {
244-
debug!("Health check failed for {}: {}", backend.url, e);
269+
Err(error) => {
270+
match error {
271+
HealthProbeError::Timeout => {
272+
debug!(
273+
"Health check timed out for {} after {}s",
274+
backend.url, config.timeout_seconds
275+
);
276+
}
277+
HealthProbeError::Transport(e) => {
278+
debug!("Health check failed for {}: {}", backend.url, e);
279+
}
280+
}
281+
245282
let mut health = self.health_status.write().await;
246283

247284
if let Some(backend_health) = health.get_mut(&backend.url) {
@@ -465,17 +502,32 @@ fn load_config() -> Result<Config> {
465502
}
466503

467504
fn setup_logging(config: &LoggingConfig) {
468-
let log_level = match config.level.as_str() {
505+
let level_key = config.level.to_lowercase();
506+
let log_level = match level_key.as_str() {
469507
"debug" => log::LevelFilter::Debug,
470508
"info" => log::LevelFilter::Info,
471509
"warn" => log::LevelFilter::Warn,
472510
"error" => log::LevelFilter::Error,
473511
_ => log::LevelFilter::Info,
474512
};
475513

476-
env_logger::Builder::from_default_env()
477-
.filter_level(log_level)
478-
.init();
514+
let mut builder = env_logger::Builder::from_default_env();
515+
builder.filter_level(log_level);
516+
517+
if config.format.eq_ignore_ascii_case("json") {
518+
builder.format(|buf, record| {
519+
let timestamp = Utc::now().to_rfc3339();
520+
let payload = serde_json::json!({
521+
"timestamp": timestamp,
522+
"level": record.level().to_string(),
523+
"target": record.target(),
524+
"message": record.args().to_string(),
525+
});
526+
writeln!(buf, "{}", payload)
527+
});
528+
}
529+
530+
builder.init();
479531
}
480532

481533
#[tokio::main]

0 commit comments

Comments
 (0)