From 21c08911dffc0a8a50ff6162a76f5c7a9aa8d5fe Mon Sep 17 00:00:00 2001 From: Gabriel Kaszewski Date: Thu, 18 Jun 2026 23:14:43 +0200 Subject: [PATCH] add tracing, env config, dotenvy bootstrap: tracing-subscriber with RUST_LOG env filter, ServerConfig from env vars (KFRAME_DATABASE_URL, KFRAME_TCP_ADDR, etc.), dotenvy for .env file loading. Replaced all println with tracing macros. tcp-server: replaced println with tracing::info/warn. Added .env.example and .gitignore for db files. --- .env.example | 8 ++ .gitignore | 4 + Cargo.lock | 102 +++++++++++++++++++++++ Cargo.toml | 3 + crates/adapters/tcp-server/Cargo.toml | 1 + crates/adapters/tcp-server/src/server.rs | 9 +- crates/bootstrap/Cargo.toml | 3 + crates/bootstrap/src/config.rs | 25 ++++++ crates/bootstrap/src/main.rs | 39 ++++++--- crates/bootstrap/src/polling.rs | 14 ++-- 10 files changed, 188 insertions(+), 20 deletions(-) create mode 100644 .env.example create mode 100644 crates/bootstrap/src/config.rs diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..ed74e98 --- /dev/null +++ b/.env.example @@ -0,0 +1,8 @@ +# K-Frame Server Configuration +KFRAME_DATABASE_URL=sqlite:kframe.db?mode=rwc +KFRAME_TCP_ADDR=0.0.0.0:2699 +KFRAME_HTTP_ADDR=0.0.0.0:3000 +KFRAME_POLL_INTERVAL_SECS=5 + +# Logging (tracing-subscriber) +RUST_LOG=info,sqlx=warn diff --git a/.gitignore b/.gitignore index 2f7896d..dd76ea2 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,5 @@ target/ +*.db +*.db-shm +*.db-wal +.env diff --git a/Cargo.lock b/Cargo.lock index 089135c..16cd04b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,15 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + [[package]] name = "allocator-api2" version = "0.2.21" @@ -154,10 +163,13 @@ dependencies = [ "application", "config-sqlite", "domain", + "dotenvy", "http-api", "http-json", "tcp-server", "tokio", + "tracing", + "tracing-subscriber", ] [[package]] @@ -1065,6 +1077,15 @@ version = "0.4.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a" +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + [[package]] name = "matchit" version = "0.8.4" @@ -1133,6 +1154,15 @@ dependencies = [ "tempfile", ] +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "num-bigint-dig" version = "0.8.6" @@ -1432,6 +1462,23 @@ dependencies = [ "bitflags", ] +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + [[package]] name = "reqwest" version = "0.12.28" @@ -1704,6 +1751,15 @@ dependencies = [ "digest", ] +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + [[package]] name = "shlex" version = "2.0.1" @@ -2047,6 +2103,7 @@ dependencies = [ "protocol", "thiserror", "tokio", + "tracing", ] [[package]] @@ -2082,6 +2139,15 @@ dependencies = [ "syn", ] +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + [[package]] name = "tinystr" version = "0.8.3" @@ -2253,6 +2319,36 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" dependencies = [ "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", ] [[package]] @@ -2318,6 +2414,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + [[package]] name = "vcpkg" version = "0.2.15" diff --git a/Cargo.toml b/Cargo.toml index 80e88d8..794e2f9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,6 +41,9 @@ tower-http = { version = "0.6", features = ["cors"] } api-types = { path = "crates/api-types" } thiserror = "2.0" anyhow = "1.0" +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter"] } +dotenvy = "0.15" serde = { version = "1.0", default-features = false, features = ["derive", "alloc"] } serde_json = "1.0" sqlx = { version = "0.8", features = ["runtime-tokio-rustls", "sqlite"] } diff --git a/crates/adapters/tcp-server/Cargo.toml b/crates/adapters/tcp-server/Cargo.toml index 0422d35..2fcea30 100644 --- a/crates/adapters/tcp-server/Cargo.toml +++ b/crates/adapters/tcp-server/Cargo.toml @@ -9,3 +9,4 @@ protocol.workspace = true tokio.workspace = true postcard.workspace = true thiserror.workspace = true +tracing.workspace = true diff --git a/crates/adapters/tcp-server/src/server.rs b/crates/adapters/tcp-server/src/server.rs index a7c6b66..33b1b0d 100644 --- a/crates/adapters/tcp-server/src/server.rs +++ b/crates/adapters/tcp-server/src/server.rs @@ -2,6 +2,7 @@ use std::sync::Arc; use tokio::net::TcpListener; use tokio::sync::broadcast; use tokio::io::AsyncWriteExt; +use tracing::{info, warn}; use crate::broadcaster::TcpBroadcaster; use crate::error::TcpServerError; @@ -10,11 +11,11 @@ pub async fn run_tcp_server( broadcaster: Arc, ) -> Result<(), TcpServerError> { let listener = TcpListener::bind(addr).await.map_err(TcpServerError::Io)?; - println!("TCP server listening on {addr}"); + info!(addr, "TCP server listening"); loop { let (mut socket, peer) = listener.accept().await.map_err(TcpServerError::Io)?; - println!("Client connected: {peer}"); + info!(%peer, "client connected"); let mut rx = broadcaster.subscribe(); @@ -23,13 +24,13 @@ pub async fn run_tcp_server( match rx.recv().await { Ok(frame) => { if socket.write_all(&frame).await.is_err() { - println!("Client disconnected: {peer}"); + info!(%peer, "client disconnected"); break; } } Err(broadcast::error::RecvError::Closed) => break, Err(broadcast::error::RecvError::Lagged(n)) => { - println!("Client {peer} lagged by {n} messages"); + warn!(%peer, skipped = n, "client lagged"); } } } diff --git a/crates/bootstrap/Cargo.toml b/crates/bootstrap/Cargo.toml index 97a5935..4abfde2 100644 --- a/crates/bootstrap/Cargo.toml +++ b/crates/bootstrap/Cargo.toml @@ -12,3 +12,6 @@ http-api.workspace = true http-json.workspace = true tokio.workspace = true anyhow.workspace = true +tracing.workspace = true +tracing-subscriber.workspace = true +dotenvy.workspace = true diff --git a/crates/bootstrap/src/config.rs b/crates/bootstrap/src/config.rs new file mode 100644 index 0000000..5a52e60 --- /dev/null +++ b/crates/bootstrap/src/config.rs @@ -0,0 +1,25 @@ +use std::env; + +pub struct ServerConfig { + pub database_url: String, + pub tcp_addr: String, + pub http_addr: String, + pub poll_interval_secs: u64, +} + +impl ServerConfig { + pub fn from_env() -> Self { + Self { + database_url: env::var("KFRAME_DATABASE_URL") + .unwrap_or_else(|_| "sqlite:kframe.db?mode=rwc".into()), + tcp_addr: env::var("KFRAME_TCP_ADDR") + .unwrap_or_else(|_| "0.0.0.0:2699".into()), + http_addr: env::var("KFRAME_HTTP_ADDR") + .unwrap_or_else(|_| "0.0.0.0:3000".into()), + poll_interval_secs: env::var("KFRAME_POLL_INTERVAL_SECS") + .ok() + .and_then(|v| v.parse().ok()) + .unwrap_or(5), + } + } +} diff --git a/crates/bootstrap/src/main.rs b/crates/bootstrap/src/main.rs index da98c1f..8a76008 100644 --- a/crates/bootstrap/src/main.rs +++ b/crates/bootstrap/src/main.rs @@ -1,3 +1,4 @@ +mod config; mod polling; use std::sync::Arc; @@ -7,34 +8,50 @@ use config_sqlite::SqliteConfigStore; use tcp_server::{TcpBroadcaster, TcpEventBus, run_tcp_server}; use http_api::AppState; use tokio::sync::Mutex; - -const DB_PATH: &str = "sqlite:kframe.db?mode=rwc"; -const TCP_ADDR: &str = "0.0.0.0:2699"; -const HTTP_ADDR: &str = "0.0.0.0:3000"; +use tracing::{info, error}; #[tokio::main] async fn main() -> Result<()> { - let config_store = Arc::new(SqliteConfigStore::new(DB_PATH).await?); + dotenvy::dotenv().ok(); + + tracing_subscriber::fmt() + .with_env_filter( + tracing_subscriber::EnvFilter::try_from_default_env() + .unwrap_or_else(|_| "info,sqlx=warn".into()), + ) + .init(); + + let cfg = config::ServerConfig::from_env(); + + info!(db = %cfg.database_url, "connecting to database"); + let config_store = Arc::new(SqliteConfigStore::new(&cfg.database_url).await?); + let event_bus = Arc::new(TcpEventBus::new(64)); let broadcaster = Arc::new(TcpBroadcaster::new(64)); let projection = Arc::new(Mutex::new(DataProjection::new())); + let tcp_addr = cfg.tcp_addr.clone(); let tcp_bc = broadcaster.clone(); tokio::spawn(async move { - run_tcp_server(TCP_ADDR, tcp_bc).await.unwrap(); + if let Err(e) = run_tcp_server(&tcp_addr, tcp_bc).await { + error!(error = %e, "tcp server failed"); + } }); - println!("TCP server on {TCP_ADDR}"); + info!(addr = %cfg.tcp_addr, "TCP server started"); + let http_addr = cfg.http_addr.clone(); let http_state = AppState { config: config_store.clone(), events: event_bus.clone(), }; tokio::spawn(async move { - http_api::serve(HTTP_ADDR, http_state).await.unwrap(); + if let Err(e) = http_api::serve(&http_addr, http_state).await { + error!(error = %e, "HTTP API failed"); + } }); - println!("HTTP API on {HTTP_ADDR}"); + info!(addr = %cfg.http_addr, "HTTP API started"); - println!("K-Frame server running"); + info!("K-Frame server running"); - polling::run(config_store, broadcaster, projection).await + polling::run(config_store, broadcaster, projection, cfg.poll_interval_secs).await } diff --git a/crates/bootstrap/src/polling.rs b/crates/bootstrap/src/polling.rs index c557d00..b13b9ad 100644 --- a/crates/bootstrap/src/polling.rs +++ b/crates/bootstrap/src/polling.rs @@ -10,18 +10,21 @@ use http_json::HttpJsonAdapter; use tcp_server::TcpBroadcaster; use config_sqlite::SqliteConfigStore; use tokio::sync::Mutex; - -const POLL_CHECK_INTERVAL: Duration = Duration::from_secs(5); +use tracing::{info, warn, debug}; pub async fn run( config: Arc, broadcaster: Arc, projection: Arc>, + poll_interval_secs: u64, ) -> Result<()> { let http_adapter = HttpJsonAdapter::new(); + let interval = Duration::from_secs(poll_interval_secs); + + info!(interval_secs = poll_interval_secs, "polling loop started"); loop { - tokio::time::sleep(POLL_CHECK_INTERVAL).await; + tokio::time::sleep(interval).await; let sources = config.list_data_sources().await .map_err(|e| anyhow::anyhow!("{e}"))?; @@ -31,6 +34,7 @@ pub async fn run( .map_err(|e| anyhow::anyhow!("{e}"))?; if sources.is_empty() || widgets.is_empty() { + debug!("no sources or widgets configured, skipping poll"); continue; } @@ -40,7 +44,7 @@ pub async fn run( let result = match poll_source(&http_adapter, source).await { Ok(v) => v, Err(e) => { - eprintln!("poll error for '{}': {e}", source.name); + warn!(source = %source.name, error = %e, "poll failed"); continue; } }; @@ -55,7 +59,7 @@ pub async fn run( broadcaster.push_screen_update(l, &all_changed).await .map_err(|e| anyhow::anyhow!("{e}"))?; } - println!("pushed {} widget updates", all_changed.len()); + info!(count = all_changed.len(), "pushed widget updates"); } } }