Skip to main content

nautilus_common/logging/
headers.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! Standard startup, system, and version headers for log output.
17
18#[cfg(all(target_os = "linux", target_env = "gnu"))]
19use std::ffi::CStr;
20use std::sync::atomic::{AtomicBool, Ordering};
21
22use nautilus_core::{UUID4, consts::NAUTILUS_VERSION_CORE};
23use nautilus_model::{
24    identifiers::TraderId,
25    types::fixed::{FIXED_PRECISION, HIGH_PRECISION_MODE, PRECISION_BYTES},
26};
27use sysinfo::System;
28use ustr::Ustr;
29
30use crate::{enums::LogColor, logging::log_info};
31
32const GIT_COMMIT_LEN: usize = 12;
33static MIMALLOC_REGISTERED: AtomicBool = AtomicBool::new(false);
34
35#[cfg(panic = "abort")]
36const PANIC_STRATEGY: &str = "abort";
37#[cfg(panic = "unwind")]
38const PANIC_STRATEGY: &str = "unwind";
39
40const BUILD_VERSIONS: &[(&str, &str)] = &[
41    ("git_commit", env!("NAUTILUS_BUILD_GIT_COMMIT")),
42    ("rustc", env!("NAUTILUS_BUILD_RUSTC_VERSION")),
43    ("target", env!("NAUTILUS_BUILD_TARGET")),
44    ("profile", env!("NAUTILUS_BUILD_PROFILE")),
45    ("panic", PANIC_STRATEGY),
46    ("cargo_lock", env!("NAUTILUS_BUILD_CARGO_LOCK_CRC32")),
47    #[cfg(all(target_os = "linux", target_env = "gnu"))]
48    ("libc_crate", env!("NAUTILUS_BUILD_LIBC_VERSION")),
49    ("rust_decimal", env!("NAUTILUS_BUILD_RUST_DECIMAL_VERSION")),
50];
51
52#[cfg(feature = "live")]
53const LIVE_VERSIONS: &[(&str, &str)] = &[
54    ("rustls", env!("NAUTILUS_BUILD_RUSTLS_VERSION")),
55    ("aws_lc_rs", env!("NAUTILUS_BUILD_AWS_LC_RS_VERSION")),
56    ("tokio", env!("NAUTILUS_BUILD_TOKIO_VERSION")),
57];
58
59#[cfg(feature = "python")]
60const PYTHON_VERSIONS: &[(&str, &str)] = &[
61    ("pyo3", env!("NAUTILUS_BUILD_PYO3_VERSION")),
62    (
63        "pyo3_async_runtimes",
64        env!("NAUTILUS_BUILD_PYO3_ASYNC_RUNTIMES_VERSION"),
65    ),
66];
67
68/// Records that the final artifact uses mimalloc as its Rust global allocator.
69///
70/// This only affects version header metadata. The final artifact must declare mimalloc as its
71/// global allocator before calling this function.
72pub fn register_allocator_mimalloc() {
73    MIMALLOC_REGISTERED.store(true, Ordering::Release);
74}
75
76/// Logs the Nautilus startup header with system, identifier, and version details.
77#[rustfmt::skip]
78pub fn log_header(trader_id: TraderId, machine_id: &str, instance_id: UUID4, component: Ustr) {
79    let mut sys = System::new();
80    sys.refresh_cpu_all();
81    sys.refresh_memory();
82
83    let c = component;
84
85    let kernel_version = System::kernel_version().map_or(String::new(), |v| format!("kernel-{v} "));
86    let os_version = System::long_os_version().unwrap_or_default();
87    let pid = std::process::id();
88
89    header_sepr(c, "=================================================================");
90    header_sepr(c, " NAUTILUS TRADER - Automated Algorithmic Trading Platform");
91    header_sepr(c, " by Nautech Systems Pty Ltd.");
92    header_sepr(c, " Copyright (C) 2015-2026. All rights reserved.");
93    header_sepr(c, "=================================================================");
94    header_line(c, "⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⣀⣀⡤⠤⠤⠤⠤⠤⠤⠤⢤⡀⠀⠀⠀⠀⠀⠀");
95    header_line(c, "⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣠⠤⠖⠚⠉⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣸⠁⠀⠀⠀⠀⠀⠀");
96    header_line(c, "⠀⠀⠀⠀⠀⠀⢀⣠⠖⠋⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⡴⠚⠁⠀⠀⠀⠀⠀⠀⠀");
97    header_line(c, "⠀⠀⠀⠀⢀⡴⠋⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⡞⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀");
98    header_line(c, "⠀⠀⠀⣠⠏⠀⠀⠀⠀⠀⠀⠀⠀⢀⣠⠤⠖⠒⠒⠒⠒⠋⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀");
99    header_line(c, "⠀⠀⠀⡇⠀⠀⠀⠀⠀⠀⠀⢀⡖⠋⣠⢴⢪⠞⣩⢟⡭⠵⢤⣤⣀⠀⠀⠀⠀⠀⠀⠀⠀⠀");
100    header_line(c, "⠀⠀⣦⡙⠦⣄⡀⠀⢀⣠⠴⠋⢠⡐⣇⢸⡘⢦⡇⣏⡴⣋⡭⠖⠮⢥⡀⠀⠀⠀⠀⠀⠀⠀");
101    header_line(c, "⠀⠀⢸⡉⠓⠦⠭⠭⠭⠴⣺⠃⠸⣷⢬⣓⣛⠒⠩⣌⢡⡷⢒⣫⠭⣝⡛⠆⠀⠀⠀⠀⠀⠀");
102    header_line(c, "⠀⠀⠀⠙⠦⢤⣀⣠⠤⠞⣡⠞⡆⠺⣭⣭⡷⢇⣷⡻⠡⠾⣛⣒⠦⢤⡙⡆⠀⠀⠀⠀⠀⠀");
103    header_line(c, "⠀⠀⠀⠈⠳⣖⠒⠒⠒⠋⠁⣠⠇⡼⢦⠀⣌⡉⢥⣄⣛⡻⢥⡈⠉⢳⡙⠇⠀⠀⠀⠀⠀⠀");
104    header_line(c, "⠀⠀⠀⠀⠀⠈⠙⣒⣒⣒⣋⡥⠞⠁⣸⠃⡇⠙⢦⢹⠀⠙⡆⢳⢀⡴⠃⠀⠀⠀⠀⠀⠀⠀");
105    header_line(c, "⠀⠀⠀⠀⠀⠀⠀⠈⠙⠒⠦⠤⠴⣚⣡⠞⠁⣠⠏⡼⢀⣠⠇⠞⠋⠀⠀⠀⠀⠀⠀⠀⠀⠀");
106    header_line(c, "⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠉⠉⠉⠉⠉⠑⠚⠋⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀");
107    header_sepr(c, "=================================================================");
108    header_sepr(c, " SYSTEM SPECIFICATION");
109    header_sepr(c, "=================================================================");
110
111    if let Some(cpu) = sys.cpus().first() {
112        header_line(c, &format!("CPU architecture: {}", cpu.brand()));
113        header_line(c, &format!("CPU(s): {} @ {} MHz", sys.cpus().len(), cpu.frequency()));
114    } else {
115        header_line(c, "CPU: unknown");
116    }
117    header_line(c, &format!("OS: {kernel_version}{os_version}"));
118
119    log_sysinfo(component);
120
121    header_sepr(c, "=================================================================");
122    header_sepr(c, " IDENTIFIERS");
123    header_sepr(c, "=================================================================");
124    header_line(c, &format!("trader_id: {trader_id}"));
125    header_line(c, &format!("machine_id: {machine_id}"));
126    header_line(c, &format!("instance_id: {instance_id}"));
127    header_line(c, &format!("PID: {pid}"));
128
129    header_sepr(c, "=================================================================");
130    header_sepr(c, " VERSIONING");
131    header_sepr(c, "=================================================================");
132
133    #[cfg(not(feature = "python"))]
134    log_rust_versioning(c);
135
136    #[cfg(feature = "python")]
137    if python_available() {
138        log_python_versioning(c);
139    } else {
140        log_rust_versioning(c);
141    }
142
143    header_sepr(c, "=================================================================");
144}
145
146#[rustfmt::skip]
147fn log_rust_versioning(c: Ustr) {
148    header_line(c, &format!("nautilus_trader: {NAUTILUS_VERSION_CORE}"));
149    log_build_versioning(c);
150    log_optional_versioning(c);
151}
152
153#[cfg(feature = "python")]
154#[rustfmt::skip]
155fn log_python_versioning(c: Ustr) {
156    let package = "nautilus_trader";
157    header_line(c, &format!("{package}: {}", python_package_version(package)));
158    header_line(c, &format!("nautilus_core: {NAUTILUS_VERSION_CORE}"));
159    header_line(c, &format!("python: {}", python_version()));
160    log_build_versioning(c);
161    log_optional_versioning(c);
162
163    // Transitional: these optional-package lines will be removed once v1 support is dropped.
164    for package in ["numpy", "pandas", "msgspec", "pyarrow", "pytz", "uvloop"] {
165        if let Some(version) = python_package_version_opt(package) {
166            header_line(c, &format!("{package}: {version}"));
167        }
168    }
169}
170
171#[rustfmt::skip]
172fn log_build_versioning(c: Ustr) {
173    for (name, version) in build_versions() {
174        header_line(c, &format!("{name}: {version}"));
175    }
176}
177
178#[rustfmt::skip]
179fn log_optional_versioning(c: Ustr) {
180    #[cfg(feature = "build-info-event-store")]
181    header_line(c, &format!("event_store: {}", event_store_version()));
182
183    #[cfg(feature = "live")]
184    for (name, version) in live_versions() {
185        header_line(c, &format!("{name}: {version}"));
186    }
187
188    #[cfg(feature = "python")]
189    for (name, version) in python_versions() {
190        header_line(c, &format!("{name}: {version}"));
191    }
192
193    #[cfg(not(any(
194        feature = "live",
195        feature = "python",
196        feature = "build-info-event-store"
197    )))]
198    let _ = c;
199}
200
201#[cfg(feature = "live")]
202fn live_versions() -> impl Iterator<Item = (&'static str, &'static str)> {
203    LIVE_VERSIONS
204        .iter()
205        .copied()
206        .filter(|(_, version)| !version.is_empty())
207}
208
209#[cfg(feature = "python")]
210fn python_versions() -> impl Iterator<Item = (&'static str, &'static str)> {
211    PYTHON_VERSIONS
212        .iter()
213        .copied()
214        .filter(|(_, version)| !version.is_empty())
215}
216
217#[cfg(feature = "build-info-event-store")]
218fn event_store_version() -> String {
219    let version = env!("NAUTILUS_BUILD_REDB_VERSION");
220    if version.is_empty() {
221        "redb".to_string()
222    } else {
223        format!("redb {version}")
224    }
225}
226
227fn build_versions() -> Vec<(&'static str, String)> {
228    let mut versions = BUILD_VERSIONS
229        .iter()
230        .filter(|(_, version)| !version.is_empty())
231        .map(|(name, version)| (*name, display_version(name, version)))
232        .collect::<Vec<_>>();
233
234    let precision_index = versions
235        .iter()
236        .position(|(name, _)| *name == "panic")
237        .map_or(versions.len(), |index| index + 1);
238    versions.insert(precision_index, ("precision", precision_version()));
239
240    let allocator_index = versions
241        .iter()
242        .position(|(name, _)| *name == "precision")
243        .map_or(versions.len(), |index| index + 1);
244    versions.insert(allocator_index, ("allocator", allocator_version()));
245
246    #[cfg(all(target_os = "linux", target_env = "gnu"))]
247    {
248        if let Some(version) = libc_runtime_version() {
249            let index = versions
250                .iter()
251                .position(|(name, _)| matches!(*name, "cargo_lock" | "libc_crate"))
252                .unwrap_or(versions.len());
253            versions.insert(index, ("libc_runtime", version));
254        }
255    }
256
257    versions
258}
259
260fn precision_version() -> String {
261    let mode = if HIGH_PRECISION_MODE == 1 {
262        "high"
263    } else {
264        "standard"
265    };
266    let bits = PRECISION_BYTES * 8;
267    format!("{mode} ({bits}-bit, {FIXED_PRECISION} dp)")
268}
269
270fn allocator_version() -> String {
271    allocator_version_for(MIMALLOC_REGISTERED.load(Ordering::Acquire))
272}
273
274fn allocator_version_for(mimalloc: bool) -> String {
275    if mimalloc {
276        let version = env!("NAUTILUS_BUILD_MIMALLOC_VERSION");
277        if version.is_empty() {
278            "mimalloc".to_string()
279        } else {
280            format!("mimalloc {version}")
281        }
282    } else {
283        "system".to_string()
284    }
285}
286
287fn display_version(name: &str, version: &str) -> String {
288    match name {
289        "cargo_lock" => version
290            .strip_prefix("crc32:")
291            .unwrap_or(version)
292            .to_string(),
293        "git_commit" => version.get(..GIT_COMMIT_LEN).unwrap_or(version).to_string(),
294        _ => version.to_string(),
295    }
296}
297
298#[cfg(all(target_os = "linux", target_env = "gnu"))]
299#[allow(unsafe_code)]
300fn libc_runtime_version() -> Option<String> {
301    // SAFETY: glibc owns the returned static null-terminated string.
302    let version = unsafe { libc::gnu_get_libc_version() };
303    if version.is_null() {
304        return None;
305    }
306
307    // SAFETY: glibc documents the returned pointer as a valid C string.
308    let version = unsafe { CStr::from_ptr(version) }.to_str().ok()?;
309    Some(format!("glibc {version}"))
310}
311
312#[cfg(feature = "python")]
313#[inline]
314#[allow(unsafe_code)]
315fn python_available() -> bool {
316    // SAFETY: `Py_IsInitialized` reads a flag and is safe to call at any
317    // time, even before the interpreter has been started.
318    unsafe { pyo3::ffi::Py_IsInitialized() != 0 }
319}
320
321/// Logs current memory and swap usage.
322#[rustfmt::skip]
323pub fn log_sysinfo(component: Ustr) {
324    let mut sys = System::new();
325    sys.refresh_memory();
326
327    let c = component;
328
329    let ram_total = sys.total_memory();
330    let ram_used = sys.used_memory();
331    let ram_used_p = (ram_used as f64 / ram_total as f64) * 100.0;
332    let ram_avail = ram_total - ram_used;
333    let ram_avail_p = (ram_avail as f64 / ram_total as f64) * 100.0;
334
335    header_sepr(c, "=================================================================");
336    header_sepr(c, " MEMORY USAGE");
337    header_sepr(c, "=================================================================");
338    header_line(c, &format!("RAM-Total: {:.2} GiB", bytes_to_gib(ram_total)));
339    header_line(c, &format!("RAM-Used: {:.2} GiB ({:.2}%)", bytes_to_gib(ram_used), ram_used_p));
340    header_line(c, &format!("RAM-Avail: {:.2} GiB ({:.2}%)", bytes_to_gib(ram_avail), ram_avail_p));
341
342    let swap_total = sys.total_swap();
343    if swap_total > 0 {
344        let swap_used = sys.used_swap();
345        let swap_used_p = (swap_used as f64 / swap_total as f64) * 100.0;
346        let swap_avail = swap_total.saturating_sub(swap_used);
347        let swap_avail_p = (swap_avail as f64 / swap_total as f64) * 100.0;
348        header_line(c, &format!("Swap-Total: {:.2} GiB", bytes_to_gib(swap_total)));
349        header_line(c, &format!("Swap-Used: {:.2} GiB ({:.2}%)", bytes_to_gib(swap_used), swap_used_p));
350        header_line(c, &format!("Swap-Avail: {:.2} GiB ({:.2}%)", bytes_to_gib(swap_avail), swap_avail_p));
351    } else {
352        header_line(c, "Swap: disabled");
353    }
354}
355
356fn header_sepr(c: Ustr, s: &str) {
357    log_info!("{}", s, color = LogColor::Cyan, component = c.as_str());
358}
359
360fn header_line(c: Ustr, s: &str) {
361    log_info!("{}", s, component = c.as_str());
362}
363
364fn bytes_to_gib(b: u64) -> f64 {
365    b as f64 / (2u64.pow(30) as f64)
366}
367
368#[cfg(feature = "python")]
369fn python_package_version(package: &str) -> String {
370    nautilus_core::python::version::get_python_package_version(package)
371}
372
373#[cfg(feature = "python")]
374fn python_package_version_opt(package: &str) -> Option<String> {
375    nautilus_core::python::version::get_python_package_version_opt(package)
376}
377
378#[cfg(feature = "python")]
379fn python_version() -> String {
380    nautilus_core::python::version::get_python_version()
381}
382
383#[cfg(test)]
384mod tests {
385    use rstest::rstest;
386
387    use super::*;
388
389    #[rstest]
390    fn test_build_versions_match_compiled_metadata() {
391        let versions = build_versions();
392
393        for (name, expected) in BUILD_VERSIONS {
394            let expected = (!expected.is_empty()).then(|| display_version(name, expected));
395            assert_eq!(version(&versions, name), expected.as_deref());
396        }
397    }
398
399    #[rstest]
400    fn test_build_modes_match_compiled_configuration() {
401        let versions = build_versions();
402        let panic_index = versions
403            .iter()
404            .position(|(name, _)| *name == "panic")
405            .expect("panic version should be present");
406        let precision_index = versions
407            .iter()
408            .position(|(name, _)| *name == "precision")
409            .expect("precision version should be present");
410        let allocator_index = versions
411            .iter()
412            .position(|(name, _)| *name == "allocator")
413            .expect("allocator version should be present");
414
415        assert_eq!(version(&versions, "panic"), Some(PANIC_STRATEGY));
416        assert_eq!(precision_index, panic_index + 1);
417        assert_eq!(allocator_index, precision_index + 1);
418        assert_eq!(
419            version(&versions, "precision"),
420            Some(precision_version().as_str()),
421        );
422        assert_eq!(version(&versions, "allocator"), Some("system"),);
423    }
424
425    #[rstest]
426    fn test_allocator_version_formats() {
427        let mimalloc = env!("NAUTILUS_BUILD_MIMALLOC_VERSION");
428        let expected = if mimalloc.is_empty() {
429            "mimalloc".to_string()
430        } else {
431            format!("mimalloc {mimalloc}")
432        };
433
434        assert_eq!(allocator_version_for(false), "system");
435        assert_eq!(allocator_version_for(true), expected);
436    }
437
438    #[cfg(feature = "live")]
439    #[rstest]
440    fn test_live_versions_match_compiled_metadata() {
441        let versions = live_versions().collect::<Vec<_>>();
442
443        for (_, expected) in LIVE_VERSIONS {
444            assert!(!expected.is_empty());
445        }
446        assert_eq!(versions, LIVE_VERSIONS);
447        assert_eq!(versions.last().map(|(name, _)| *name), Some("tokio"));
448    }
449
450    #[cfg(feature = "python")]
451    #[rstest]
452    fn test_python_versions_follow_tokio() {
453        let live_versions = live_versions().collect::<Vec<_>>();
454        let python_versions = python_versions().collect::<Vec<_>>();
455
456        for (_, expected) in PYTHON_VERSIONS {
457            assert!(!expected.is_empty());
458        }
459        assert_eq!(python_versions, PYTHON_VERSIONS);
460        assert_eq!(live_versions.last().map(|(name, _)| *name), Some("tokio"));
461        assert_eq!(
462            python_versions
463                .iter()
464                .map(|(name, _)| *name)
465                .collect::<Vec<_>>(),
466            ["pyo3", "pyo3_async_runtimes"],
467        );
468    }
469
470    #[cfg(feature = "build-info-event-store")]
471    #[rstest]
472    fn test_event_store_version_matches_compiled_metadata() {
473        let redb = env!("NAUTILUS_BUILD_REDB_VERSION");
474
475        assert!(!redb.is_empty());
476        assert_eq!(event_store_version(), format!("redb {redb}"));
477    }
478
479    #[rstest]
480    fn test_cargo_lock_fingerprint_format() {
481        const CRC32_HEX_LEN: usize = 8;
482
483        let versions = build_versions();
484        let Some(fingerprint) = version(&versions, "cargo_lock") else {
485            return;
486        };
487        let embedded = env!("NAUTILUS_BUILD_CARGO_LOCK_CRC32");
488        let Some(checksum) = embedded.strip_prefix("crc32:") else {
489            panic!("embedded cargo_lock fingerprint should use the crc32 prefix");
490        };
491
492        assert_eq!(checksum.len(), CRC32_HEX_LEN);
493        assert!(checksum.bytes().all(|byte| byte.is_ascii_hexdigit()));
494        assert_eq!(fingerprint.len(), CRC32_HEX_LEN);
495        assert_eq!(fingerprint, checksum);
496    }
497
498    #[rstest]
499    fn test_git_commit_format() {
500        let versions = build_versions();
501        let Some(commit) = version(&versions, "git_commit") else {
502            return;
503        };
504
505        assert_eq!(commit.len(), GIT_COMMIT_LEN);
506        assert!(commit.bytes().all(|byte| byte.is_ascii_hexdigit()));
507    }
508
509    #[cfg(all(target_os = "linux", target_env = "gnu"))]
510    #[rstest]
511    fn test_libc_runtime_version_reports_glibc() {
512        let version = libc_runtime_version().expect("glibc should report its runtime version");
513        let versions = build_versions();
514        let runtime_index = versions
515            .iter()
516            .position(|(name, _)| *name == "libc_runtime")
517            .expect("libc runtime version should be present");
518        let cargo_lock_index = versions.iter().position(|(name, _)| *name == "cargo_lock");
519        let libc_crate_index = versions.iter().position(|(name, _)| *name == "libc_crate");
520
521        assert!(version.starts_with("glibc "));
522        assert!(
523            version["glibc ".len()..]
524                .bytes()
525                .any(|byte| byte.is_ascii_digit())
526        );
527
528        if let Some(cargo_lock_index) = cargo_lock_index {
529            assert_eq!(cargo_lock_index, runtime_index + 1);
530        }
531
532        if let Some(libc_crate_index) = libc_crate_index {
533            assert_eq!(
534                libc_crate_index,
535                cargo_lock_index.unwrap_or(runtime_index) + 1,
536            );
537        }
538    }
539
540    fn version<'a>(versions: &'a [(&str, String)], name: &str) -> Option<&'a str> {
541        versions
542            .iter()
543            .find(|(candidate, _)| *candidate == name)
544            .map(|(_, version)| version.as_str())
545    }
546}