Skip to content

Commit 5536958

Browse files
committed
Add non-blocking file logging and stderr error logging
Add a lightweight logging system that tees to stderr and optionally to a file via SPICEIO_LOG_FILE. The file writer runs on a dedicated OS thread fed by a bounded mpsc channel — try_send ensures logging never blocks the proxy. When no file is configured there is zero overhead (no thread, no channel, no allocation). Also adds stderr logging for: - Connection lifecycle: client TCP accept, SMB TCP connect, negotiate, auth, tree connect - All failure paths: every SMB wire error, S3 streaming errors, body read errors, access denied, client disconnects
1 parent 67dcbee commit 5536958

6 files changed

Lines changed: 184 additions & 27 deletions

File tree

CLAUDE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ The binary requires these environment variables:
3434
- `SPICEIO_SMB_DOMAIN` — SMB domain (default empty)
3535
- `SPICEIO_BUCKET` — virtual S3 bucket name (defaults to `SPICEIO_SMB_SHARE`)
3636
- `SPICEIO_REGION` — AWS region to advertise (default `us-east-1`)
37+
- `SPICEIO_LOG_FILE` — append logs to this file in addition to stderr (optional; non-blocking, never stalls the proxy)
3738

3839
## Architecture
3940

src/lib.rs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,17 @@
1-
//! spiceio — S3-compatible API proxy to SMB 3.1.1 file shares (macOS 26).
1+
//! spiceio — S3-compatible API proxy to SMB 3.1.x file shares.
2+
//!
3+
//! Library crate exposing modules for benchmarking and testing.
4+
5+
pub mod log;
6+
7+
/// Log to stderr and optionally to a file (non-blocking).
8+
/// Drop-in replacement for `eprintln!` — same format-string syntax.
9+
#[macro_export]
10+
macro_rules! slog {
11+
($($arg:tt)*) => {
12+
$crate::log::emit(format_args!($($arg)*))
13+
};
14+
}
215

316
pub mod crypto;
417
pub mod s3;

src/log.rs

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
//! Non-blocking file logger backed by a dedicated OS thread.
2+
//!
3+
//! Design: a bounded `mpsc::sync_channel` feeds a background thread that owns
4+
//! the file descriptor and writes through a 64 KB `BufWriter`. The hot path
5+
//! (`emit`) does a single `try_send` — if the channel is full the message is
6+
//! silently dropped, so logging never blocks the proxy.
7+
//!
8+
//! When no log file is configured only stderr is written (zero overhead from
9+
//! the file path — no channel, no allocation, no thread).
10+
11+
use std::fmt;
12+
use std::fs::OpenOptions;
13+
use std::io::{BufWriter, Write};
14+
use std::sync::OnceLock;
15+
use std::sync::mpsc;
16+
17+
/// Channel capacity — number of formatted log lines buffered before drops.
18+
const CHANNEL_CAP: usize = 4096;
19+
20+
/// BufWriter capacity — bytes buffered before a syscall write.
21+
const BUF_CAP: usize = 64 * 1024;
22+
23+
static FILE_TX: OnceLock<mpsc::SyncSender<String>> = OnceLock::new();
24+
25+
/// Initialise file logging. Call once at startup.
26+
/// If `path` is `None`, only stderr logging is active (the default).
27+
pub fn init(path: Option<&str>) {
28+
let Some(p) = path else { return };
29+
30+
let file = OpenOptions::new()
31+
.create(true)
32+
.append(true)
33+
.open(p)
34+
.unwrap_or_else(|e| panic!("[spiceio] failed to open log file {p}: {e}"));
35+
36+
let (tx, rx) = mpsc::sync_channel::<String>(CHANNEL_CAP);
37+
38+
std::thread::Builder::new()
39+
.name("spiceio-log".into())
40+
.spawn(move || writer_loop(rx, file))
41+
.expect("[spiceio] failed to spawn log thread");
42+
43+
FILE_TX.set(tx).ok();
44+
}
45+
46+
/// Background writer — drains the channel and flushes in batches.
47+
fn writer_loop(rx: mpsc::Receiver<String>, file: std::fs::File) {
48+
let mut w = BufWriter::with_capacity(BUF_CAP, file);
49+
while let Ok(line) = rx.recv() {
50+
let _ = w.write_all(line.as_bytes());
51+
let _ = w.write_all(b"\n");
52+
// Drain any queued messages before issuing the syscall flush.
53+
while let Ok(line) = rx.try_recv() {
54+
let _ = w.write_all(line.as_bytes());
55+
let _ = w.write_all(b"\n");
56+
}
57+
let _ = w.flush();
58+
}
59+
}
60+
61+
/// Write a formatted message to stderr and (if configured) to the log file.
62+
/// The file send is non-blocking — messages are dropped if the channel is full.
63+
#[inline]
64+
pub fn emit(args: fmt::Arguments<'_>) {
65+
eprintln!("{args}");
66+
if let Some(tx) = FILE_TX.get() {
67+
// Allocate only when file logging is active.
68+
let _ = tx.try_send(args.to_string());
69+
}
70+
}

src/main.rs

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
//! spiceio — S3-compatible API proxy to SMB 3.1.1 file shares (macOS 26).
22
33
mod crypto;
4+
mod log;
45
mod s3;
56
mod smb;
67

@@ -20,6 +21,15 @@ use s3::router::AppState;
2021
use smb::client::SmbConfig;
2122
use smb::ops::ShareSession;
2223

24+
/// Log to stderr and optionally to a file (non-blocking).
25+
/// Drop-in replacement for `eprintln!` — same format-string syntax.
26+
#[macro_export]
27+
macro_rules! slog {
28+
($($arg:tt)*) => {
29+
$crate::log::emit(format_args!($($arg)*))
30+
};
31+
}
32+
2333
/// Runtime configuration parsed from environment variables.
2434
struct Config {
2535
/// Address to bind the HTTP server to
@@ -73,11 +83,16 @@ async fn main() {
7383
return;
7484
}
7585

86+
log::init(env::var("SPICEIO_LOG_FILE").ok().as_deref());
87+
7688
let config = Config::from_env();
7789

78-
eprintln!(
90+
slog!(
7991
"[spiceio] connecting to smb://{}@{}:{}/{}",
80-
config.smb_username, config.smb_server, config.smb_port, config.smb_share
92+
config.smb_username,
93+
config.smb_server,
94+
config.smb_port,
95+
config.smb_share
8196
);
8297

8398
// Connect to SMB server
@@ -114,20 +129,24 @@ async fn main() {
114129
.await
115130
.expect("failed to bind TCP listener");
116131

117-
eprintln!("[spiceio] listening on http://{bind_addr}");
118-
eprintln!(
132+
slog!("[spiceio] listening on http://{bind_addr}");
133+
slog!(
119134
"[spiceio] bucket: {} region: {}",
120-
config.bucket_name, config.region
135+
config.bucket_name,
136+
config.region
121137
);
122138

123139
// Accept loop
124140
loop {
125141
tokio::select! {
126142
accepted = listener.accept() => {
127143
let (stream, peer_addr) = match accepted {
128-
Ok(v) => v,
144+
Ok(v) => {
145+
slog!("[spiceio] client connected: {}", v.1);
146+
v
147+
}
129148
Err(e) => {
130-
eprintln!("[spiceio] accept error: {e}");
149+
slog!("[spiceio] accept error: {e}");
131150
continue;
132151
}
133152
};
@@ -148,12 +167,12 @@ async fn main() {
148167
.serve_connection(io, service)
149168
.await
150169
&& !e.to_string().contains("connection reset") {
151-
eprintln!("[spiceio] connection error from {peer_addr}: {e}");
170+
slog!("[spiceio] connection error from {peer_addr}: {e}");
152171
}
153172
});
154173
}
155174
_ = signal::ctrl_c() => {
156-
eprintln!("\n[spiceio] shutting down");
175+
slog!("\n[spiceio] shutting down");
157176
break;
158177
}
159178
}

src/s3/router.rs

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -597,10 +597,14 @@ async fn handle_get_object(
597597
Ok(chunk) => {
598598
offset += chunk.len() as u64;
599599
if tx.send(chunk).await.is_err() {
600-
break; // Client disconnected
600+
crate::slog!("[spiceio] getobject client disconnected");
601+
break;
601602
}
602603
}
603-
Err(_) => break,
604+
Err(e) => {
605+
crate::slog!("[spiceio] getobject read error: {e}");
606+
break;
607+
}
604608
}
605609
}
606610
let _ = handle.close().await;
@@ -722,6 +726,7 @@ async fn handle_put_object(
722726
}
723727
}
724728
Err(e) => {
729+
crate::slog!("[spiceio] putobject body read error: {e}");
725730
let _ = handle.close().await;
726731
return io_to_s3_error(&io::Error::other(format!("body read error: {e}")));
727732
}
@@ -1422,10 +1427,11 @@ fn io_to_s3_error(e: &io::Error) -> Response<SpiceioBody> {
14221427
"The specified key does not exist.",
14231428
),
14241429
io::ErrorKind::PermissionDenied => {
1430+
crate::slog!("[spiceio] access denied: {e}");
14251431
error_response(StatusCode::FORBIDDEN, "AccessDenied", "Access Denied")
14261432
}
14271433
_ => {
1428-
eprintln!("[spiceio] error: {e}");
1434+
crate::slog!("[spiceio] error: {e}");
14291435
error_response(
14301436
StatusCode::INTERNAL_SERVER_ERROR,
14311437
"InternalError",
@@ -1477,7 +1483,7 @@ async fn collect_body(req: Request<Incoming>) -> Bytes {
14771483
match req.into_body().collect().await {
14781484
Ok(collected) => collected.to_bytes(),
14791485
Err(e) => {
1480-
eprintln!("[spiceio] body collect error: {e}");
1486+
crate::slog!("[spiceio] body collect error: {e}");
14811487
Bytes::new()
14821488
}
14831489
}

0 commit comments

Comments
 (0)