diff --git a/Cargo.lock b/Cargo.lock index aeca49f..80f299d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2343,6 +2343,18 @@ dependencies = [ "trx-core", ] +[[package]] +name = "trx-decode-log" +version = "0.1.0" +dependencies = [ + "chrono", + "dirs", + "serde", + "serde_json", + "tracing", + "trx-core", +] + [[package]] name = "trx-frontend" version = "0.1.0" @@ -2433,6 +2445,7 @@ dependencies = [ "trx-backend", "trx-core", "trx-cw", + "trx-decode-log", "trx-ft8", "trx-protocol", "trx-wspr", diff --git a/Cargo.toml b/Cargo.toml index dead104..b3d8ec6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,7 @@ members = [ "src/decoders/trx-aprs", "src/decoders/trx-cw", + "src/decoders/trx-decode-log", "src/decoders/trx-ft8", "src/decoders/trx-wspr", "src/trx-core", diff --git a/src/decoders/trx-decode-log/Cargo.toml b/src/decoders/trx-decode-log/Cargo.toml new file mode 100644 index 0000000..3996d97 --- /dev/null +++ b/src/decoders/trx-decode-log/Cargo.toml @@ -0,0 +1,16 @@ +# SPDX-FileCopyrightText: 2026 Stanislaw Grams +# +# SPDX-License-Identifier: BSD-2-Clause + +[package] +name = "trx-decode-log" +version = "0.1.0" +edition = "2021" + +[dependencies] +trx-core = { path = "../../trx-core" } +chrono = { version = "0.4", default-features = false, features = ["clock"] } +dirs = "6" +serde = { workspace = true, features = ["derive"] } +serde_json = { workspace = true } +tracing = { workspace = true } diff --git a/src/trx-server/src/decode_logs.rs b/src/decoders/trx-decode-log/src/lib.rs similarity index 67% rename from src/trx-server/src/decode_logs.rs rename to src/decoders/trx-decode-log/src/lib.rs index 0b447b9..3f91a84 100644 --- a/src/trx-server/src/decode_logs.rs +++ b/src/decoders/trx-decode-log/src/lib.rs @@ -1,7 +1,12 @@ -// SPDX-FileCopyrightText: 2025 Stanislaw Grams +// SPDX-FileCopyrightText: 2026 Stanislaw Grams // // SPDX-License-Identifier: BSD-2-Clause +//! Server-side decoder file logging (APRS / CW / FT8 / WSPR). +//! +//! Provides [`DecodeLogsConfig`] for TOML configuration and [`DecoderLoggers`] +//! for writing JSON-Lines log files with automatic daily rotation. + use std::fs::{create_dir_all, File, OpenOptions}; use std::io::{BufWriter, Write}; use std::path::{Path, PathBuf}; @@ -9,12 +14,62 @@ use std::sync::{Arc, Mutex}; use std::time::{SystemTime, UNIX_EPOCH}; use chrono::Utc; +use serde::{Deserialize, Serialize}; use serde_json::json; use tracing::warn; -use crate::config::DecodeLogsConfig; use trx_core::decode::{AprsPacket, CwEvent, Ft8Message, WsprMessage}; +// --------------------------------------------------------------------------- +// Configuration +// --------------------------------------------------------------------------- + +fn default_decode_logs_dir() -> String { + if let Some(data_dir) = dirs::data_dir() { + return data_dir + .join("trx-rs") + .join("decoders") + .to_string_lossy() + .to_string(); + } + "logs/decoders".to_string() +} + +/// Server-side decoder file logging configuration. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(default)] +pub struct DecodeLogsConfig { + /// Whether decoder file logging is enabled + pub enabled: bool, + /// Base directory for log files + pub dir: String, + /// APRS decoder log filename + pub aprs_file: String, + /// CW decoder log filename + pub cw_file: String, + /// FT8 decoder log filename + pub ft8_file: String, + /// WSPR decoder log filename + pub wspr_file: String, +} + +impl Default for DecodeLogsConfig { + fn default() -> Self { + Self { + enabled: false, + dir: default_decode_logs_dir(), + aprs_file: "TRXRS-APRS-%YYYY%-%MM%-%DD%.log".to_string(), + cw_file: "TRXRS-CW-%YYYY%-%MM%-%DD%.log".to_string(), + ft8_file: "TRXRS-FT8-%YYYY%-%MM%-%DD%.log".to_string(), + wspr_file: "TRXRS-WSPR-%YYYY%-%MM%-%DD%.log".to_string(), + } + } +} + +// --------------------------------------------------------------------------- +// File logger (private) +// --------------------------------------------------------------------------- + struct DecoderFileLogger { base_dir: PathBuf, file_template: String, @@ -64,7 +119,7 @@ impl DecoderFileLogger { }) } - fn write_payload(&self, payload: &T) { + fn write_payload(&self, payload: &T) { let ts_ms = match SystemTime::now().duration_since(UNIX_EPOCH) { Ok(d) => d.as_millis() as u64, Err(_) => 0, @@ -106,6 +161,11 @@ impl DecoderFileLogger { } } +// --------------------------------------------------------------------------- +// Public API +// --------------------------------------------------------------------------- + +/// Aggregate logger for all four server-side decoders. pub struct DecoderLoggers { aprs: DecoderFileLogger, cw: DecoderFileLogger, @@ -114,6 +174,7 @@ pub struct DecoderLoggers { } impl DecoderLoggers { + /// Create loggers from config, or return `None` when logging is disabled. pub fn from_config(cfg: &DecodeLogsConfig) -> Result>, String> { if !cfg.enabled { return Ok(None); diff --git a/src/trx-server/Cargo.toml b/src/trx-server/Cargo.toml index a79e5e3..7388b57 100644 --- a/src/trx-server/Cargo.toml +++ b/src/trx-server/Cargo.toml @@ -26,6 +26,7 @@ trx-backend = { path = "trx-backend" } trx-core = { path = "../trx-core" } trx-aprs = { path = "../decoders/trx-aprs" } trx-cw = { path = "../decoders/trx-cw" } +trx-decode-log = { path = "../decoders/trx-decode-log" } trx-ft8 = { path = "../decoders/trx-ft8" } trx-wspr = { path = "../decoders/trx-wspr" } trx-protocol = { path = "../trx-protocol" } diff --git a/src/trx-server/src/audio.rs b/src/trx-server/src/audio.rs index 95ff018..3260988 100644 --- a/src/trx-server/src/audio.rs +++ b/src/trx-server/src/audio.rs @@ -28,7 +28,7 @@ use trx_ft8::Ft8Decoder; use trx_wspr::WsprDecoder; use crate::config::AudioConfig; -use crate::decode_logs::DecoderLoggers; +use trx_decode_log::DecoderLoggers; const APRS_HISTORY_RETENTION: Duration = Duration::from_secs(24 * 60 * 60); const FT8_HISTORY_RETENTION: Duration = Duration::from_secs(24 * 60 * 60); diff --git a/src/trx-server/src/config.rs b/src/trx-server/src/config.rs index a41373f..0fea2af 100644 --- a/src/trx-server/src/config.rs +++ b/src/trx-server/src/config.rs @@ -16,6 +16,7 @@ use std::path::{Path, PathBuf}; use serde::{Deserialize, Serialize}; use trx_app::{ConfigError, ConfigFile}; +pub use trx_decode_log::DecodeLogsConfig; use trx_core::rig::state::RigMode; @@ -260,48 +261,6 @@ impl Default for AprsFiConfig { } } -fn default_decode_logs_dir() -> String { - if let Some(data_dir) = dirs::data_dir() { - return data_dir - .join("trx-rs") - .join("decoders") - .to_string_lossy() - .to_string(); - } - "logs/decoders".to_string() -} - -/// Server-side decoder file logging configuration. -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(default)] -pub struct DecodeLogsConfig { - /// Whether decoder file logging is enabled - pub enabled: bool, - /// Base directory for log files - pub dir: String, - /// APRS decoder log filename - pub aprs_file: String, - /// CW decoder log filename - pub cw_file: String, - /// FT8 decoder log filename - pub ft8_file: String, - /// WSPR decoder log filename - pub wspr_file: String, -} - -impl Default for DecodeLogsConfig { - fn default() -> Self { - Self { - enabled: false, - dir: default_decode_logs_dir(), - aprs_file: "TRXRS-APRS-%YYYY%-%MM%-%DD%.log".to_string(), - cw_file: "TRXRS-CW-%YYYY%-%MM%-%DD%.log".to_string(), - ft8_file: "TRXRS-FT8-%YYYY%-%MM%-%DD%.log".to_string(), - wspr_file: "TRXRS-WSPR-%YYYY%-%MM%-%DD%.log".to_string(), - } - } -} - impl ServerConfig { pub fn validate(&self) -> Result<(), String> { validate_log_level(self.general.log_level.as_deref())?; diff --git a/src/trx-server/src/main.rs b/src/trx-server/src/main.rs index 1c07111..63451b1 100644 --- a/src/trx-server/src/main.rs +++ b/src/trx-server/src/main.rs @@ -5,7 +5,6 @@ mod aprsfi; mod audio; mod config; -mod decode_logs; mod error; mod listener; mod pskreporter; @@ -34,7 +33,7 @@ use trx_core::rig::state::RigState; use trx_core::DynResult; use config::ServerConfig; -use decode_logs::DecoderLoggers; +use trx_decode_log::DecoderLoggers; const PKG_DESCRIPTION: &str = concat!(env!("CARGO_PKG_NAME"), " - rig server daemon"); const RIG_TASK_CHANNEL_BUFFER: usize = 32;