[refactor](decoders): extract decoder logging into trx-decode-log crate

Move DecoderLoggers and DecodeLogsConfig out of trx-server into a
dedicated src/decoders/trx-decode-log crate, giving file logging the
same standalone crate treatment as the four decoder crates.

- src/decoders/trx-decode-log/ (new — DecodeLogsConfig + DecoderLoggers)
- trx-server/config.rs: re-exports DecodeLogsConfig from trx-decode-log
  so ServerConfig field references and all tests compile unchanged
- trx-server: drop decode_logs module, use trx_decode_log directly

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-23 18:36:04 +01:00
parent f4b92a0f20
commit b9005acffd
8 changed files with 98 additions and 48 deletions
+1
View File
@@ -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" }
+1 -1
View File
@@ -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);
+1 -42
View File
@@ -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())?;
-151
View File
@@ -1,151 +0,0 @@
// SPDX-FileCopyrightText: 2025 Stanislaw Grams <stanislawgrams@gmail.com>
//
// SPDX-License-Identifier: BSD-2-Clause
use std::fs::{create_dir_all, File, OpenOptions};
use std::io::{BufWriter, Write};
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};
use std::time::{SystemTime, UNIX_EPOCH};
use chrono::Utc;
use serde_json::json;
use tracing::warn;
use crate::config::DecodeLogsConfig;
use trx_core::decode::{AprsPacket, CwEvent, Ft8Message, WsprMessage};
struct DecoderFileLogger {
base_dir: PathBuf,
file_template: String,
state: Mutex<DecoderFileState>,
label: &'static str,
}
struct DecoderFileState {
current_file_name: String,
writer: BufWriter<File>,
}
impl DecoderFileLogger {
fn resolve_file_name(template: &str) -> String {
let now = Utc::now();
template
.replace("%YYYY%", &now.format("%Y").to_string())
.replace("%MM%", &now.format("%m").to_string())
.replace("%DD%", &now.format("%d").to_string())
}
fn open_writer(path: &Path, label: &'static str) -> Result<BufWriter<File>, String> {
if let Some(parent) = path.parent() {
create_dir_all(parent)
.map_err(|e| format!("create {} log dir '{}': {}", label, parent.display(), e))?;
}
let file = OpenOptions::new()
.create(true)
.append(true)
.open(path)
.map_err(|e| format!("open {} log '{}': {}", label, path.display(), e))?;
Ok(BufWriter::new(file))
}
fn open(base_dir: &Path, template: &str, label: &'static str) -> Result<Self, String> {
let file_name = Self::resolve_file_name(template);
let path = base_dir.join(&file_name);
let writer = Self::open_writer(&path, label)?;
Ok(Self {
base_dir: base_dir.to_path_buf(),
file_template: template.to_string(),
state: Mutex::new(DecoderFileState {
current_file_name: file_name,
writer,
}),
label,
})
}
fn write_payload<T: serde::Serialize>(&self, payload: &T) {
let ts_ms = match SystemTime::now().duration_since(UNIX_EPOCH) {
Ok(d) => d.as_millis() as u64,
Err(_) => 0,
};
let line = json!({
"ts_ms": ts_ms,
"decoder": self.label,
"payload": payload,
});
let Ok(mut state) = self.state.lock() else {
warn!("decode log mutex poisoned for {}", self.label);
return;
};
let next_file_name = Self::resolve_file_name(&self.file_template);
if next_file_name != state.current_file_name {
let next_path = self.base_dir.join(&next_file_name);
match Self::open_writer(&next_path, self.label) {
Ok(next_writer) => {
state.current_file_name = next_file_name;
state.writer = next_writer;
}
Err(e) => {
warn!("decode log reopen failed for {}: {}", self.label, e);
return;
}
}
}
if serde_json::to_writer(&mut state.writer, &line).is_err() {
warn!("decode log serialization failed for {}", self.label);
return;
}
if state.writer.write_all(b"\n").is_err() {
warn!("decode log write failed for {}", self.label);
return;
}
let _ = state.writer.flush();
}
}
pub struct DecoderLoggers {
aprs: DecoderFileLogger,
cw: DecoderFileLogger,
ft8: DecoderFileLogger,
wspr: DecoderFileLogger,
}
impl DecoderLoggers {
pub fn from_config(cfg: &DecodeLogsConfig) -> Result<Option<Arc<Self>>, String> {
if !cfg.enabled {
return Ok(None);
}
let base_dir = PathBuf::from(cfg.dir.trim());
create_dir_all(&base_dir)
.map_err(|e| format!("create decode log dir '{}': {}", base_dir.display(), e))?;
let loggers = Self {
aprs: DecoderFileLogger::open(&base_dir, &cfg.aprs_file, "aprs")?,
cw: DecoderFileLogger::open(&base_dir, &cfg.cw_file, "cw")?,
ft8: DecoderFileLogger::open(&base_dir, &cfg.ft8_file, "ft8")?,
wspr: DecoderFileLogger::open(&base_dir, &cfg.wspr_file, "wspr")?,
};
Ok(Some(Arc::new(loggers)))
}
pub fn log_aprs(&self, pkt: &AprsPacket) {
self.aprs.write_payload(pkt);
}
pub fn log_cw(&self, evt: &CwEvent) {
self.cw.write_payload(evt);
}
pub fn log_ft8(&self, msg: &Ft8Message) {
self.ft8.write_payload(msg);
}
pub fn log_wspr(&self, msg: &WsprMessage) {
self.wspr.write_payload(msg);
}
}
+1 -2
View File
@@ -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;