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.
This commit is contained in:
2026-06-18 23:14:43 +02:00
parent 15b75d860c
commit 21c08911df
10 changed files with 188 additions and 20 deletions

8
.env.example Normal file
View File

@@ -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

4
.gitignore vendored
View File

@@ -1 +1,5 @@
target/ target/
*.db
*.db-shm
*.db-wal
.env

102
Cargo.lock generated
View File

@@ -2,6 +2,15 @@
# It is not intended for manual editing. # It is not intended for manual editing.
version = 4 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]] [[package]]
name = "allocator-api2" name = "allocator-api2"
version = "0.2.21" version = "0.2.21"
@@ -154,10 +163,13 @@ dependencies = [
"application", "application",
"config-sqlite", "config-sqlite",
"domain", "domain",
"dotenvy",
"http-api", "http-api",
"http-json", "http-json",
"tcp-server", "tcp-server",
"tokio", "tokio",
"tracing",
"tracing-subscriber",
] ]
[[package]] [[package]]
@@ -1065,6 +1077,15 @@ version = "0.4.32"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a" 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]] [[package]]
name = "matchit" name = "matchit"
version = "0.8.4" version = "0.8.4"
@@ -1133,6 +1154,15 @@ dependencies = [
"tempfile", "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]] [[package]]
name = "num-bigint-dig" name = "num-bigint-dig"
version = "0.8.6" version = "0.8.6"
@@ -1432,6 +1462,23 @@ dependencies = [
"bitflags", "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]] [[package]]
name = "reqwest" name = "reqwest"
version = "0.12.28" version = "0.12.28"
@@ -1704,6 +1751,15 @@ dependencies = [
"digest", "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]] [[package]]
name = "shlex" name = "shlex"
version = "2.0.1" version = "2.0.1"
@@ -2047,6 +2103,7 @@ dependencies = [
"protocol", "protocol",
"thiserror", "thiserror",
"tokio", "tokio",
"tracing",
] ]
[[package]] [[package]]
@@ -2082,6 +2139,15 @@ dependencies = [
"syn", "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]] [[package]]
name = "tinystr" name = "tinystr"
version = "0.8.3" version = "0.8.3"
@@ -2253,6 +2319,36 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
dependencies = [ dependencies = [
"once_cell", "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]] [[package]]
@@ -2318,6 +2414,12 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
[[package]]
name = "valuable"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
[[package]] [[package]]
name = "vcpkg" name = "vcpkg"
version = "0.2.15" version = "0.2.15"

View File

@@ -41,6 +41,9 @@ tower-http = { version = "0.6", features = ["cors"] }
api-types = { path = "crates/api-types" } api-types = { path = "crates/api-types" }
thiserror = "2.0" thiserror = "2.0"
anyhow = "1.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 = { version = "1.0", default-features = false, features = ["derive", "alloc"] }
serde_json = "1.0" serde_json = "1.0"
sqlx = { version = "0.8", features = ["runtime-tokio-rustls", "sqlite"] } sqlx = { version = "0.8", features = ["runtime-tokio-rustls", "sqlite"] }

View File

@@ -9,3 +9,4 @@ protocol.workspace = true
tokio.workspace = true tokio.workspace = true
postcard.workspace = true postcard.workspace = true
thiserror.workspace = true thiserror.workspace = true
tracing.workspace = true

View File

@@ -2,6 +2,7 @@ use std::sync::Arc;
use tokio::net::TcpListener; use tokio::net::TcpListener;
use tokio::sync::broadcast; use tokio::sync::broadcast;
use tokio::io::AsyncWriteExt; use tokio::io::AsyncWriteExt;
use tracing::{info, warn};
use crate::broadcaster::TcpBroadcaster; use crate::broadcaster::TcpBroadcaster;
use crate::error::TcpServerError; use crate::error::TcpServerError;
@@ -10,11 +11,11 @@ pub async fn run_tcp_server(
broadcaster: Arc<TcpBroadcaster>, broadcaster: Arc<TcpBroadcaster>,
) -> Result<(), TcpServerError> { ) -> Result<(), TcpServerError> {
let listener = TcpListener::bind(addr).await.map_err(TcpServerError::Io)?; let listener = TcpListener::bind(addr).await.map_err(TcpServerError::Io)?;
println!("TCP server listening on {addr}"); info!(addr, "TCP server listening");
loop { loop {
let (mut socket, peer) = listener.accept().await.map_err(TcpServerError::Io)?; let (mut socket, peer) = listener.accept().await.map_err(TcpServerError::Io)?;
println!("Client connected: {peer}"); info!(%peer, "client connected");
let mut rx = broadcaster.subscribe(); let mut rx = broadcaster.subscribe();
@@ -23,13 +24,13 @@ pub async fn run_tcp_server(
match rx.recv().await { match rx.recv().await {
Ok(frame) => { Ok(frame) => {
if socket.write_all(&frame).await.is_err() { if socket.write_all(&frame).await.is_err() {
println!("Client disconnected: {peer}"); info!(%peer, "client disconnected");
break; break;
} }
} }
Err(broadcast::error::RecvError::Closed) => break, Err(broadcast::error::RecvError::Closed) => break,
Err(broadcast::error::RecvError::Lagged(n)) => { Err(broadcast::error::RecvError::Lagged(n)) => {
println!("Client {peer} lagged by {n} messages"); warn!(%peer, skipped = n, "client lagged");
} }
} }
} }

View File

@@ -12,3 +12,6 @@ http-api.workspace = true
http-json.workspace = true http-json.workspace = true
tokio.workspace = true tokio.workspace = true
anyhow.workspace = true anyhow.workspace = true
tracing.workspace = true
tracing-subscriber.workspace = true
dotenvy.workspace = true

View File

@@ -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),
}
}
}

View File

@@ -1,3 +1,4 @@
mod config;
mod polling; mod polling;
use std::sync::Arc; use std::sync::Arc;
@@ -7,34 +8,50 @@ use config_sqlite::SqliteConfigStore;
use tcp_server::{TcpBroadcaster, TcpEventBus, run_tcp_server}; use tcp_server::{TcpBroadcaster, TcpEventBus, run_tcp_server};
use http_api::AppState; use http_api::AppState;
use tokio::sync::Mutex; use tokio::sync::Mutex;
use tracing::{info, error};
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";
#[tokio::main] #[tokio::main]
async fn main() -> Result<()> { 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 event_bus = Arc::new(TcpEventBus::new(64));
let broadcaster = Arc::new(TcpBroadcaster::new(64)); let broadcaster = Arc::new(TcpBroadcaster::new(64));
let projection = Arc::new(Mutex::new(DataProjection::new())); let projection = Arc::new(Mutex::new(DataProjection::new()));
let tcp_addr = cfg.tcp_addr.clone();
let tcp_bc = broadcaster.clone(); let tcp_bc = broadcaster.clone();
tokio::spawn(async move { 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 { let http_state = AppState {
config: config_store.clone(), config: config_store.clone(),
events: event_bus.clone(), events: event_bus.clone(),
}; };
tokio::spawn(async move { 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}"); }
});
println!("K-Frame server running"); info!(addr = %cfg.http_addr, "HTTP API started");
polling::run(config_store, broadcaster, projection).await info!("K-Frame server running");
polling::run(config_store, broadcaster, projection, cfg.poll_interval_secs).await
} }

View File

@@ -10,18 +10,21 @@ use http_json::HttpJsonAdapter;
use tcp_server::TcpBroadcaster; use tcp_server::TcpBroadcaster;
use config_sqlite::SqliteConfigStore; use config_sqlite::SqliteConfigStore;
use tokio::sync::Mutex; use tokio::sync::Mutex;
use tracing::{info, warn, debug};
const POLL_CHECK_INTERVAL: Duration = Duration::from_secs(5);
pub async fn run( pub async fn run(
config: Arc<SqliteConfigStore>, config: Arc<SqliteConfigStore>,
broadcaster: Arc<TcpBroadcaster>, broadcaster: Arc<TcpBroadcaster>,
projection: Arc<Mutex<DataProjection>>, projection: Arc<Mutex<DataProjection>>,
poll_interval_secs: u64,
) -> Result<()> { ) -> Result<()> {
let http_adapter = HttpJsonAdapter::new(); let http_adapter = HttpJsonAdapter::new();
let interval = Duration::from_secs(poll_interval_secs);
info!(interval_secs = poll_interval_secs, "polling loop started");
loop { loop {
tokio::time::sleep(POLL_CHECK_INTERVAL).await; tokio::time::sleep(interval).await;
let sources = config.list_data_sources().await let sources = config.list_data_sources().await
.map_err(|e| anyhow::anyhow!("{e}"))?; .map_err(|e| anyhow::anyhow!("{e}"))?;
@@ -31,6 +34,7 @@ pub async fn run(
.map_err(|e| anyhow::anyhow!("{e}"))?; .map_err(|e| anyhow::anyhow!("{e}"))?;
if sources.is_empty() || widgets.is_empty() { if sources.is_empty() || widgets.is_empty() {
debug!("no sources or widgets configured, skipping poll");
continue; continue;
} }
@@ -40,7 +44,7 @@ pub async fn run(
let result = match poll_source(&http_adapter, source).await { let result = match poll_source(&http_adapter, source).await {
Ok(v) => v, Ok(v) => v,
Err(e) => { Err(e) => {
eprintln!("poll error for '{}': {e}", source.name); warn!(source = %source.name, error = %e, "poll failed");
continue; continue;
} }
}; };
@@ -55,7 +59,7 @@ pub async fn run(
broadcaster.push_screen_update(l, &all_changed).await broadcaster.push_screen_update(l, &all_changed).await
.map_err(|e| anyhow::anyhow!("{e}"))?; .map_err(|e| anyhow::anyhow!("{e}"))?;
} }
println!("pushed {} widget updates", all_changed.len()); info!(count = all_changed.len(), "pushed widget updates");
} }
} }
} }