Skip to content

Commit 1f66215

Browse files
committed
feat(s5): pluggable message stores and logs (Stage S5)
Tasks T053–T058, T060, T061 (US7). truefix-store: - async MessageStore trait (seqnums + sent-message persistence for resend) + StoreConfig/build_store factory. - MemoryStore, FileStore, CachedFileStore, NoopStore. FileStore replays an append log on open and tolerates a torn trailing record (recovers the good prefix, flags was_corrupted) — supports ForceResendWhenCorruptedStore (T060). - SQL store via sqlx/SQLite behind the `sql` feature (T057); compiles and its restart test passes under `--features sql`. Off by default to keep the core build light and license-clean. truefix-log: - Log trait with message/event stream separation (FR-H2) + LogConfig/build_log. - ScreenLog, FileLog (separate messages.log/event.log), TracingLog, CompositeLog (fan-out). Integration (T061): - Session::seed_sequences restores persisted seqnums. - Transport gains opt-in Services (store + log): seeds seqnums on connect, persists them each event, logs inbound/outbound; existing entry points unchanged. Tests: 7 store tests (+1 SQL under feature), 3 log tests, a transport restart-continuity test, and a session seed test. 86 workspace tests green; fmt/clippy -D warnings clean. Deferred (unchecked): T059 SQL log — the sync Log trait vs async sqlx needs an async logging path / background writer; planned refinement. Cross-restart resend of message *bytes* (restoring them into the engine) is also future work; S5 wires sequence-number continuity.
1 parent 8f98383 commit 1f66215

27 files changed

Lines changed: 84672 additions & 26 deletions

.codely-cli/auto-saves/chat-auto-save-2026-06-29-21-57-19-495-explore-aw1hj8.json

Lines changed: 12495 additions & 0 deletions
Large diffs are not rendered by default.

.codely-cli/auto-saves/chat-auto-save-2026-06-29-22-05-17-296-explore-ca404u.json

Lines changed: 27851 additions & 0 deletions
Large diffs are not rendered by default.

.codely-cli/auto-saves/chat-auto-save-2026-06-29-22-06-09-791-explore-ae89ae.json

Lines changed: 17039 additions & 0 deletions
Large diffs are not rendered by default.

.codely-cli/auto-saves/chat-auto-save-2026-06-29-22-06-35-561-explore-4snuru.json

Lines changed: 24712 additions & 0 deletions
Large diffs are not rendered by default.

Cargo.lock

Lines changed: 732 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ truefix-dict = { path = "crates/truefix-dict" }
3636
truefix-session = { path = "crates/truefix-session" }
3737
truefix-transport = { path = "crates/truefix-transport" }
3838
truefix-config = { path = "crates/truefix-config" }
39+
truefix-store = { path = "crates/truefix-store" }
40+
truefix-log = { path = "crates/truefix-log" }
3941

4042
[profile.release]
4143
# Production builds: keep debug info off, optimize. Benchmarks live in benches/.

crates/truefix-log/Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,9 @@ repository.workspace = true
88
description = "FIX logging (screen/file/tracing/composite)."
99
publish = false
1010

11+
[dependencies]
12+
thiserror.workspace = true
13+
tracing.workspace = true
14+
1115
[lints]
1216
workspace = true
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
//! Fan-out log.
2+
3+
use crate::Log;
4+
5+
/// Fans every entry out to each wrapped log.
6+
pub struct CompositeLog {
7+
logs: Vec<Box<dyn Log>>,
8+
}
9+
10+
impl CompositeLog {
11+
/// Create a composite over `logs`.
12+
pub fn new(logs: Vec<Box<dyn Log>>) -> Self {
13+
Self { logs }
14+
}
15+
}
16+
17+
impl Log for CompositeLog {
18+
fn on_incoming(&self, message: &str) {
19+
for log in &self.logs {
20+
log.on_incoming(message);
21+
}
22+
}
23+
fn on_outgoing(&self, message: &str) {
24+
for log in &self.logs {
25+
log.on_outgoing(message);
26+
}
27+
}
28+
fn on_event(&self, text: &str) {
29+
for log in &self.logs {
30+
log.on_event(text);
31+
}
32+
}
33+
}

crates/truefix-log/src/file.rs

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
//! File log: separate `messages.log` and `event.log` streams (FR-H2).
2+
3+
use std::fs::{File, OpenOptions};
4+
use std::io::Write;
5+
use std::path::Path;
6+
use std::sync::Mutex;
7+
8+
use crate::{Log, LogError};
9+
10+
/// Logs inbound/outbound messages to `messages.log` and events to `event.log`.
11+
pub struct FileLog {
12+
messages: Mutex<File>,
13+
events: Mutex<File>,
14+
}
15+
16+
impl FileLog {
17+
/// Open (creating if needed) the log files in `dir`.
18+
pub fn open(dir: &Path) -> Result<Self, LogError> {
19+
std::fs::create_dir_all(dir).map_err(|e| LogError::Io(e.to_string()))?;
20+
let messages = open_append(&dir.join("messages.log"))?;
21+
let events = open_append(&dir.join("event.log"))?;
22+
Ok(Self {
23+
messages: Mutex::new(messages),
24+
events: Mutex::new(events),
25+
})
26+
}
27+
}
28+
29+
fn open_append(path: &Path) -> Result<File, LogError> {
30+
OpenOptions::new()
31+
.create(true)
32+
.append(true)
33+
.open(path)
34+
.map_err(|e| LogError::Io(e.to_string()))
35+
}
36+
37+
fn write_line(file: &Mutex<File>, line: &str) {
38+
if let Ok(mut f) = file.lock() {
39+
let _ = writeln!(f, "{line}");
40+
}
41+
}
42+
43+
impl Log for FileLog {
44+
fn on_incoming(&self, message: &str) {
45+
write_line(&self.messages, &format!("I {message}"));
46+
}
47+
fn on_outgoing(&self, message: &str) {
48+
write_line(&self.messages, &format!("O {message}"));
49+
}
50+
fn on_event(&self, text: &str) {
51+
write_line(&self.events, text);
52+
}
53+
}

crates/truefix-log/src/lib.rs

Lines changed: 73 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
1-
//! FIX logging (screen/file/tracing/composite).
1+
//! `truefix-log` — pluggable FIX logging with message/event stream separation (FR-H2).
22
//!
3-
//! Part of the TrueFix FIX engine. Design: `specs/001-fix-engine-parity/`.
4-
//! This crate is scaffolding (Stage S0); functionality lands in later stages.
3+
//! The [`Log`] trait separates **message** logging (inbound/outbound wire messages) from
4+
//! **event** logging (session lifecycle, errors). Implementations: [`ScreenLog`], [`FileLog`],
5+
//! [`TracingLog`], and [`CompositeLog`] (fan-out).
6+
//!
7+
//! Design: `specs/001-fix-engine-parity/`.
58
#![cfg_attr(
69
not(test),
710
deny(
@@ -11,3 +14,70 @@
1114
clippy::indexing_slicing
1215
)
1316
)]
17+
18+
mod composite;
19+
mod file;
20+
mod screen;
21+
mod tracing_log;
22+
23+
use std::path::PathBuf;
24+
25+
use thiserror::Error;
26+
27+
pub use composite::CompositeLog;
28+
pub use file::FileLog;
29+
pub use screen::ScreenLog;
30+
pub use tracing_log::TracingLog;
31+
32+
/// An error constructing a log.
33+
#[derive(Debug, Error)]
34+
pub enum LogError {
35+
/// An I/O error (e.g. opening a log file).
36+
#[error("log I/O error: {0}")]
37+
Io(String),
38+
}
39+
40+
/// A FIX log sink. Message logging (incoming/outgoing) is kept separate from event logging.
41+
///
42+
/// Methods are best-effort and infallible from the caller's perspective; a sink that cannot
43+
/// write drops the entry rather than failing the session.
44+
pub trait Log: Send + Sync {
45+
/// Log an inbound wire message.
46+
fn on_incoming(&self, message: &str);
47+
/// Log an outbound wire message.
48+
fn on_outgoing(&self, message: &str);
49+
/// Log a session event.
50+
fn on_event(&self, text: &str);
51+
}
52+
53+
/// Which log backend to construct.
54+
#[derive(Debug, Clone)]
55+
pub enum LogConfig {
56+
/// Log to stdout/stderr.
57+
Screen,
58+
/// Log to files in a directory (`messages.log` + `event.log`).
59+
File {
60+
/// Directory holding the log files.
61+
dir: PathBuf,
62+
},
63+
/// Log via the `tracing` facade.
64+
Tracing,
65+
/// Fan out to several logs.
66+
Composite(Vec<LogConfig>),
67+
}
68+
69+
/// Build a boxed [`Log`] from a [`LogConfig`].
70+
pub fn build_log(config: &LogConfig) -> Result<Box<dyn Log>, LogError> {
71+
Ok(match config {
72+
LogConfig::Screen => Box::new(ScreenLog::new()),
73+
LogConfig::File { dir } => Box::new(FileLog::open(dir)?),
74+
LogConfig::Tracing => Box::new(TracingLog::new()),
75+
LogConfig::Composite(parts) => {
76+
let mut logs: Vec<Box<dyn Log>> = Vec::with_capacity(parts.len());
77+
for part in parts {
78+
logs.push(build_log(part)?);
79+
}
80+
Box::new(CompositeLog::new(logs))
81+
}
82+
})
83+
}

0 commit comments

Comments
 (0)