refactor: remove log dependency, replace with custom macros

- Added custom logging macros (log_error!, log_warn!, log_info!, log_debug!)
- Implemented LogLevel enum and LOG_LEVEL static for runtime level control
- Exported macros with #[macro_export] for use across all modules
- Replaced all log:: calls in websocket_client.rs, health.rs, and version.rs
- Removed obsolete test code referencing old log crate
- Removed log dependency from Cargo.toml
- Fixed useless comparison warning in test
- Binary size: still 1.5M (no change - log was lightweight)
- Tests: 65 passing, 0 warnings

This completes the removal of 9 dependencies (thiserror, hostname, chrono,
base64, rand, ureq, tiny_http, anyhow, log). Dependency count: 19 → 10.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Graham McIntire 2026-01-19 17:31:12 -06:00
parent 0daaf9ed68
commit 71267870b0
No known key found for this signature in database
6 changed files with 45 additions and 84 deletions

1
Cargo.lock generated
View file

@ -1085,7 +1085,6 @@ version = "0.1.0"
dependencies = [
"clap",
"futures",
"log",
"prost",
"prost-build",
"prost-types",

View file

@ -10,7 +10,6 @@ tokio-tungstenite = { version = "0.21", features = ["rustls-tls-webpki-roots"] }
futures = "0.3"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
log = { version = "0.4", features = ["std"] }
clap = { version = "4.0", features = ["derive", "env"] }
prost = "0.13"
prost-types = "0.13"

View file

@ -1,4 +1,3 @@
use log::info;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use std::time::Instant;
@ -19,7 +18,7 @@ pub async fn start_health_server(port: u16) -> Result<()> {
let start_time = Arc::new(Instant::now());
let addr = format!("0.0.0.0:{}", port);
info!("Starting health endpoint on {}", addr);
crate::log_info!("Starting health endpoint on {}", addr);
let listener = TcpListener::bind(&addr).await?;
loop {
@ -68,7 +67,7 @@ pub async fn start_health_server(port: u16) -> Result<()> {
});
}
Err(e) => {
log::warn!("Failed to accept health check connection: {}", e);
crate::log_warn!("Failed to accept health check connection: {}", e);
}
}
}

View file

@ -12,10 +12,10 @@ use tokio::time::sleep;
use websocket_client::AgentClient;
// Log levels
static LOG_LEVEL: std::sync::OnceLock<LogLevel> = std::sync::OnceLock::new();
pub(crate) static LOG_LEVEL: std::sync::OnceLock<LogLevel> = std::sync::OnceLock::new();
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
enum LogLevel {
pub(crate) enum LogLevel {
Error = 1,
Warn = 2,
Info = 3,
@ -34,40 +34,44 @@ impl LogLevel {
}
}
#[macro_export]
macro_rules! log_error {
($($arg:tt)*) => {{
let level = LOG_LEVEL.get().copied().unwrap_or(LogLevel::Info);
if level >= LogLevel::Error {
let level = $crate::LOG_LEVEL.get().copied().unwrap_or($crate::LogLevel::Info);
if level >= $crate::LogLevel::Error {
let ts = $crate::format_timestamp();
eprintln!("[{}] [ERROR] {}", ts, format!($($arg)*));
}
}};
}
#[macro_export]
macro_rules! log_warn {
($($arg:tt)*) => {{
let level = LOG_LEVEL.get().copied().unwrap_or(LogLevel::Info);
if level >= LogLevel::Warn {
let level = $crate::LOG_LEVEL.get().copied().unwrap_or($crate::LogLevel::Info);
if level >= $crate::LogLevel::Warn {
let ts = $crate::format_timestamp();
eprintln!("[{}] [WARN] {}", ts, format!($($arg)*));
}
}};
}
#[macro_export]
macro_rules! log_info {
($($arg:tt)*) => {{
let level = LOG_LEVEL.get().copied().unwrap_or(LogLevel::Info);
if level >= LogLevel::Info {
let level = $crate::LOG_LEVEL.get().copied().unwrap_or($crate::LogLevel::Info);
if level >= $crate::LogLevel::Info {
let ts = $crate::format_timestamp();
eprintln!("[{}] [INFO] {}", ts, format!($($arg)*));
}
}};
}
#[macro_export]
macro_rules! log_debug {
($($arg:tt)*) => {{
let level = LOG_LEVEL.get().copied().unwrap_or(LogLevel::Info);
if level >= LogLevel::Debug {
let level = $crate::LOG_LEVEL.get().copied().unwrap_or($crate::LogLevel::Info);
if level >= $crate::LogLevel::Debug {
let ts = $crate::format_timestamp();
eprintln!("[{}] [DEBUG] {}", ts, format!($($arg)*));
}
@ -243,44 +247,6 @@ async fn main() {
#[cfg(test)]
mod tests {
use super::*;
use log::Log;
#[test]
fn test_simple_logger_enabled() {
let logger = SimpleLogger {
level: LevelFilter::Info,
};
// Info level should be enabled
let info_metadata = log::MetadataBuilder::new()
.level(log::Level::Info)
.target("test")
.build();
assert!(logger.enabled(&info_metadata));
// Debug level should not be enabled
let debug_metadata = log::MetadataBuilder::new()
.level(log::Level::Debug)
.target("test")
.build();
assert!(!logger.enabled(&debug_metadata));
// Error level should be enabled
let error_metadata = log::MetadataBuilder::new()
.level(log::Level::Error)
.target("test")
.build();
assert!(logger.enabled(&error_metadata));
}
#[test]
fn test_simple_logger_flush() {
let logger = SimpleLogger {
level: LevelFilter::Info,
};
// flush() does nothing, just verify it's callable
logger.flush();
}
#[test]
fn test_convert_http_to_websocket() {

View file

@ -1,5 +1,3 @@
use log::info;
// Get version at runtime - prefers BUILD_VERSION from build.rs, falls back to Cargo.toml
fn current_version() -> &'static str {
option_env!("BUILD_VERSION").unwrap_or(env!("CARGO_PKG_VERSION"))
@ -8,8 +6,8 @@ fn current_version() -> &'static str {
/// Startup check - logs current version
pub fn check_for_updates() {
let current_ver = current_version();
info!("Current version: {}", current_ver);
info!("Watchtower will automatically update to new versions");
crate::log_info!("Current version: {}", current_ver);
crate::log_info!("Watchtower will automatically update to new versions");
}
#[cfg(test)]

View file

@ -59,17 +59,17 @@ impl AgentClient {
// Strip trailing slash from base URL to avoid double slashes
let base_url = url.trim_end_matches('/');
let ws_url = format!("{}/socket/agent/websocket", base_url);
log::info!("Connecting to WebSocket: {}", ws_url);
crate::log_info!("Connecting to WebSocket: {}", ws_url);
let (mut ws_stream, _) = connect_async(&ws_url)
.await
.map_err(|e| {
log::error!("WebSocket connection failed: {}", e);
crate::log_error!("WebSocket connection failed: {}", e);
e
})
.map_err(|e| format!("Failed to connect to WebSocket: {}", e))?;
log::info!("Connected to Towerops server at {}", url);
crate::log_info!("Connected to Towerops server at {}", url);
let agent_id = generate_agent_id();
let (result_tx, result_rx) = mpsc::unbounded_channel();
@ -85,7 +85,7 @@ impl AgentClient {
let join_text = serde_json::to_string(&join_msg)?;
ws_stream.send(WsMessage::Text(join_text)).await?;
log::info!(
crate::log_info!(
"Sent channel join request with token for agent:{}",
agent_id
);
@ -122,15 +122,15 @@ impl AgentClient {
self.handle_text_message(&text).await?;
}
Some(Ok(WsMessage::Close(_))) => {
log::info!("Server closed connection");
crate::log_info!("Server closed connection");
break;
}
Some(Err(e)) => {
log::error!("WebSocket error: {}", e);
crate::log_error!("WebSocket error: {}", e);
return Err(e.into());
}
None => {
log::info!("Connection closed");
crate::log_info!("Connection closed");
break;
}
_ => {}
@ -163,7 +163,7 @@ impl AgentClient {
match phoenix_msg.event.as_str() {
"phx_reply" => {
log::info!("Channel join reply: {:?}", phoenix_msg.payload);
crate::log_info!("Channel join reply: {:?}", phoenix_msg.payload);
}
"jobs" => {
// Extract binary protobuf from payload
@ -176,7 +176,7 @@ impl AgentClient {
}
}
_ => {
log::debug!("Ignoring unknown event: {}", phoenix_msg.event);
crate::log_debug!("Ignoring unknown event: {}", phoenix_msg.event);
}
}
@ -195,11 +195,11 @@ impl AgentClient {
/// Process job list from server.
async fn handle_jobs(&self, job_list: AgentJobList) -> Result<()> {
log::info!("Received {} jobs from server", job_list.jobs.len());
crate::log_info!("Received {} jobs from server", job_list.jobs.len());
for job in job_list.jobs {
let job_type = JobType::try_from(job.job_type).unwrap_or(JobType::Poll);
log::info!("Executing job: {} (type: {:?})", job.job_id, job_type);
crate::log_info!("Executing job: {} (type: {:?})", job.job_id, job_type);
// Extract monitoring configuration before moving job
let monitoring_info = job.snmp_device.as_ref().and_then(|snmp_device| {
@ -224,13 +224,13 @@ impl AgentClient {
tokio::spawn(async move {
if let Err(e) = execute_job(job, result_tx).await {
log::error!("Job execution failed: {}", e);
crate::log_error!("Job execution failed: {}", e);
}
});
// Start monitoring task if enabled
if let Some((ip, interval_seconds)) = monitoring_info {
log::info!(
crate::log_info!(
"Starting ICMP monitoring for device {} every {} seconds",
device_id,
interval_seconds
@ -246,7 +246,7 @@ impl AgentClient {
)
.await
{
log::error!("Monitoring task failed: {}", e);
crate::log_error!("Monitoring task failed: {}", e);
}
});
}
@ -277,7 +277,7 @@ impl AgentClient {
let text = serde_json::to_string(&msg)?;
self.ws_stream.send(WsMessage::Text(text)).await?;
log::debug!("Sent heartbeat");
crate::log_debug!("Sent heartbeat");
Ok(())
}
@ -295,7 +295,7 @@ impl AgentClient {
let text = serde_json::to_string(&msg)?;
self.ws_stream.send(WsMessage::Text(text)).await?;
log::debug!("Sent SNMP result for device {}", result.device_id);
crate::log_debug!("Sent SNMP result for device {}", result.device_id);
Ok(())
}
@ -313,7 +313,7 @@ impl AgentClient {
let text = serde_json::to_string(&msg)?;
self.ws_stream.send(WsMessage::Text(text)).await?;
log::debug!(
crate::log_debug!(
"Sent monitoring check for device {}: {}",
check.device_id,
check.status
@ -351,7 +351,7 @@ async fn execute_job(job: AgentJob, result_tx: mpsc::UnboundedSender<SnmpResult>
oid_values.insert(oid.clone(), value_to_string(value));
}
Err(e) => {
log::warn!("SNMP GET failed for OID {}: {}", oid, e);
crate::log_warn!("SNMP GET failed for OID {}: {}", oid, e);
}
}
}
@ -375,7 +375,7 @@ async fn execute_job(job: AgentJob, result_tx: mpsc::UnboundedSender<SnmpResult>
}
}
Err(e) => {
log::warn!("SNMP WALK failed for OID {}: {}", base_oid, e);
crate::log_warn!("SNMP WALK failed for OID {}: {}", base_oid, e);
}
}
}
@ -393,7 +393,7 @@ async fn execute_job(job: AgentJob, result_tx: mpsc::UnboundedSender<SnmpResult>
.as_secs() as i64,
};
log::info!(
crate::log_info!(
"Collected {} OID values for job {}",
result.oid_values.len(),
job.job_id
@ -549,7 +549,7 @@ async fn run_monitoring_task(
let ip_addr: std::net::IpAddr = match ip.parse() {
Ok(addr) => addr,
Err(e) => {
log::error!("Invalid IP address for device {}: {}", device_id, e);
crate::log_error!("Invalid IP address for device {}: {}", device_id, e);
continue;
}
};
@ -568,11 +568,11 @@ async fn run_monitoring_task(
};
if let Err(e) = check_tx.send(check) {
log::error!("Failed to send monitoring check: {}", e);
crate::log_error!("Failed to send monitoring check: {}", e);
break;
}
log::debug!(
crate::log_debug!(
"ICMP ping successful for {}: {:.2}ms",
ip,
rtt.as_secs_f64() * 1000.0
@ -589,11 +589,11 @@ async fn run_monitoring_task(
};
if let Err(send_err) = check_tx.send(check) {
log::error!("Failed to send monitoring check: {}", send_err);
crate::log_error!("Failed to send monitoring check: {}", send_err);
break;
}
log::warn!("ICMP ping failed for {}: {}", ip, e);
crate::log_warn!("ICMP ping failed for {}: {}", ip, e);
}
}
}
@ -663,8 +663,8 @@ mod tests {
let uptime = get_uptime_seconds();
// On Linux with /proc/uptime, should return non-zero
// On other platforms or if file doesn't exist, returns 0
// Just verify it's callable and returns a number
assert!(uptime >= 0);
// uptime is u64, so always >= 0 - just verify it's callable
let _ = uptime;
}
#[test]