Skip to main content

truefix_log/
lib.rs

1//! `truefix-log` — pluggable FIX logging with message/event stream separation (FR-H2).
2//!
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//! Audit 006 additions: [`FileLogOptions::max_size_bytes`] rotates `messages.log`/`event.log`
8//! once either exceeds the configured size (NEW-108); [`FileLog`] always timestamps event lines
9//! and surfaces write failures instead of discarding them silently (NEW-124, NEW-125); and
10//! `LogConfig` (in `truefix-config`) threads file-backend options through generic log
11//! configuration (NEW-126).
12//!
13//! Feature 012 (audit 007) additions: [`FileLog`]'s constructors (`open`/`open_with_options`, and
14//! [`build_log`] for `LogConfig::File`) are now `async` and queue writes onto a bounded channel
15//! consumed by a background writer task — the same channel + background-writer shape
16//! `SqlLog`/`MssqlLog`/`MongoLog`/`RedbLog` (each behind its own Cargo feature) already use — so
17//! `on_incoming`/`on_outgoing`/`on_event` never block the calling async task on disk I/O
18//! (NEW-156). [`Log::shutdown`] on [`FileLog`] drains that queue before returning; a background
19//! write/flush failure is surfaced via a structured `tracing::error!` event and a
20//! `truefix_log_write_failures_total` metrics counter (NEW-156, FR-009) instead of the previous
21//! best-effort `eprintln!`.
22//!
23//! Design: `specs/001-fix-engine-parity/`, `specs/012-audit-007-remediation/`.
24#![cfg_attr(
25    not(test),
26    deny(
27        clippy::unwrap_used,
28        clippy::expect_used,
29        clippy::panic,
30        clippy::indexing_slicing
31    )
32)]
33
34mod composite;
35mod file;
36#[cfg(feature = "mongodb")]
37mod mongo;
38#[cfg(feature = "mssql")]
39mod mssql;
40mod prefix;
41#[cfg(feature = "redb")]
42mod redb;
43mod screen;
44#[cfg(feature = "sql")]
45mod sql;
46mod tracing_log;
47
48use std::path::PathBuf;
49
50use thiserror::Error;
51
52pub use composite::CompositeLog;
53pub use file::{FileLog, FileLogOptions, RetentionPolicy};
54#[cfg(feature = "mongodb")]
55pub use mongo::{MongoLog, MongoLogConfig};
56#[cfg(feature = "mssql")]
57pub use mssql::{MssqlLog, MssqlLogConfig};
58pub use prefix::SessionPrefixLog;
59#[cfg(feature = "redb")]
60pub use redb::{RedbLog, RedbLogConfig};
61pub use screen::{ScreenLog, ScreenLogOptions};
62#[cfg(feature = "sql")]
63pub use sql::{SqlLog, SqlLogConfig, SqlLogPoolOptions};
64pub use tracing_log::{TracingLog, TracingLogOptions};
65
66/// Whether a raw FIX wire message is a Heartbeat (`35=0`), used by the `*LogHeartbeats`/
67/// `*ShowHeartBeats` switches (FR-026). Matches on an exact SOH-delimited field to avoid
68/// false positives from substrings like `3510=0`.
69pub(crate) fn is_heartbeat(message: &str) -> bool {
70    message.split('\u{1}').any(|field| field == "35=0")
71}
72
73/// An error constructing a log.
74#[derive(Debug, Error)]
75pub enum LogError {
76    /// An I/O error (e.g. opening a log file).
77    #[error("log I/O error: {0}")]
78    Io(String),
79}
80
81/// A FIX log sink. Message logging (incoming/outgoing) is kept separate from event logging.
82///
83/// Methods are best-effort and infallible from the caller's perspective; a sink that cannot
84/// write drops the entry rather than failing the session.
85#[async_trait::async_trait]
86pub trait Log: Send + Sync {
87    /// Log an inbound wire message.
88    fn on_incoming(&self, message: &str);
89    /// Log an outbound wire message.
90    fn on_outgoing(&self, message: &str);
91    /// Log a session event.
92    fn on_event(&self, text: &str);
93    /// NEW-91 (feature 009): flush any entries still buffered in an async background writer and
94    /// await its completion, so a shutdown doesn't drop entries queued right before it. Default
95    /// no-op — every synchronous backend (`ScreenLog`/`FileLog`/`TracingLog`) already persists
96    /// each entry before its `on_*` call returns; only the channel-backed async backends
97    /// (`Sql`/`Mssql`/`Mongo`/`Redb`Log, each behind its own Cargo feature) override this.
98    async fn shutdown(&self) {}
99}
100
101#[async_trait::async_trait]
102impl Log for Box<dyn Log> {
103    fn on_incoming(&self, message: &str) {
104        (**self).on_incoming(message);
105    }
106    fn on_outgoing(&self, message: &str) {
107        (**self).on_outgoing(message);
108    }
109    fn on_event(&self, text: &str) {
110        (**self).on_event(text);
111    }
112    async fn shutdown(&self) {
113        (**self).shutdown().await;
114    }
115}
116
117/// Which log backend to construct.
118#[derive(Debug, Clone)]
119pub enum LogConfig {
120    /// Log to stdout/stderr.
121    Screen,
122    /// Log to files in a directory (`messages.log` + `event.log`).
123    File {
124        /// Directory holding the log files.
125        dir: PathBuf,
126        /// Output switches (`FileLogHeartbeats`/`FileIncludeTimeStampForMessages`/
127        /// `FileIncludeMilliseconds`; NEW-126, audit 006) — previously this variant carried only a
128        /// directory, so every `LogConfig`-based construction path silently ignored these options
129        /// and always built a hardcoded-default `FileLog`.
130        options: FileLogOptions,
131    },
132    /// Log via the `tracing` facade.
133    Tracing,
134    /// Fan out to several logs.
135    Composite(Vec<LogConfig>),
136}
137
138/// Build a boxed [`Log`] from a [`LogConfig`].
139///
140/// `async` (NEW-156, feature 012): the `File` variant now opens a channel + background-writer
141/// backed [`FileLog`] (see its doc for why that constructor requires an active Tokio runtime).
142/// The recursive `Composite` call is boxed (`Box::pin`) since Rust can't otherwise size a
143/// self-recursive async fn's future.
144pub async fn build_log(config: &LogConfig) -> Result<Box<dyn Log>, LogError> {
145    Ok(match config {
146        LogConfig::Screen => Box::new(ScreenLog::new()),
147        LogConfig::File { dir, options } => {
148            Box::new(FileLog::open_with_options(dir, *options).await?)
149        }
150        LogConfig::Tracing => Box::new(TracingLog::new()),
151        LogConfig::Composite(parts) => {
152            let mut logs: Vec<Box<dyn Log>> = Vec::with_capacity(parts.len());
153            for part in parts {
154                logs.push(Box::pin(build_log(part)).await?);
155            }
156            Box::new(CompositeLog::new(logs))
157        }
158    })
159}