Skip to content

Commit 42dd74d

Browse files
authored
feat: Add logging module with file rotation support (#252)
Introduces a new logging module for the SPV client with: - Configurable log file rotation - Active log file: `run.log` and rotated files: `dash-spv.YYYYMMDD_HHMMSS.log` - Separate console and file output options - CLI flags: `--log-level`, `--no-log-file`, `--print-to-console`, `--max-log-files`, `--log-url`
1 parent fb0de33 commit 42dd74d

File tree

17 files changed

+837
-81
lines changed

17 files changed

+837
-81
lines changed

dash-spv-ffi/FFI_API.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1176,14 +1176,14 @@ dash_spv_ffi_enable_test_mode() -> ()
11761176
#### `dash_spv_ffi_init_logging`
11771177

11781178
```c
1179-
dash_spv_ffi_init_logging(level: *const c_char) -> i32
1179+
dash_spv_ffi_init_logging(level: *const c_char, enable_console: bool, log_dir: *const c_char, max_files: usize,) -> i32
11801180
```
11811181
11821182
**Description:**
1183-
Initialize logging for the SPV library. # Safety - `level` may be null or point to a valid, NUL-terminated C string. - If non-null, the pointer must remain valid for the duration of this call.
1183+
Initialize logging for the SPV library. # Arguments - `level`: Log level string (null uses RUST_LOG env var or defaults to INFO). Valid values: "error", "warn", "info", "debug", "trace" - `enable_console`: Whether to output logs to console (stderr) - `log_dir`: Directory for log files (null to disable file logging) - `max_files`: Maximum archived log files to retain (ignored if log_dir is null) # Safety - `level` and `log_dir` may be null or point to valid, NUL-terminated C strings.
11841184
11851185
**Safety:**
1186-
- `level` may be null or point to a valid, NUL-terminated C string. - If non-null, the pointer must remain valid for the duration of this call.
1186+
- `level` and `log_dir` may be null or point to valid, NUL-terminated C strings.
11871187
11881188
**Module:** `utils`
11891189

dash-spv-ffi/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ See `examples/basic_usage.c` for a simple example of using the FFI bindings.
4747
#include "dash_spv_ffi.h"
4848

4949
// Initialize logging
50-
dash_spv_ffi_init_logging("info");
50+
dash_spv_ffi_init_logging("info", true, NULL, 0);
5151

5252
// Create configuration
5353
FFIClientConfig* config = dash_spv_ffi_config_testnet();

dash-spv-ffi/examples/basic_usage.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
int main() {
66
// Initialize logging
7-
if (dash_spv_ffi_init_logging("info") != 0) {
7+
if (dash_spv_ffi_init_logging("info", true, NULL, 0) != 0) {
88
fprintf(stderr, "Failed to initialize logging\n");
99
return 1;
1010
}

dash-spv-ffi/include/dash_spv_ffi.h

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -979,11 +979,22 @@ void dash_spv_ffi_unconfirmed_transaction_destroy_addresses(struct FFIString *ad
979979
/**
980980
* Initialize logging for the SPV library.
981981
*
982+
* # Arguments
983+
* - `level`: Log level string (null uses RUST_LOG env var or defaults to INFO).
984+
* Valid values: "error", "warn", "info", "debug", "trace"
985+
* - `enable_console`: Whether to output logs to console (stderr)
986+
* - `log_dir`: Directory for log files (null to disable file logging)
987+
* - `max_files`: Maximum archived log files to retain (ignored if log_dir is null)
988+
*
982989
* # Safety
983-
* - `level` may be null or point to a valid, NUL-terminated C string.
984-
* - If non-null, the pointer must remain valid for the duration of this call.
990+
* - `level` and `log_dir` may be null or point to valid, NUL-terminated C strings.
985991
*/
986-
int32_t dash_spv_ffi_init_logging(const char *level) ;
992+
993+
int32_t dash_spv_ffi_init_logging(const char *level,
994+
bool enable_console,
995+
const char *log_dir,
996+
uintptr_t max_files)
997+
;
987998

988999
const char *dash_spv_ffi_version(void) ;
9891000

dash-spv-ffi/src/bin/ffi_cli.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ fn main() {
121121
// Initialize tracing/logging via FFI so `tracing::info!` emits output
122122
let level = matches.get_one::<String>("log-level").map(String::as_str).unwrap_or("info");
123123
let level_c = CString::new(level).unwrap();
124-
let _ = dash_spv_ffi_init_logging(level_c.as_ptr());
124+
let _ = dash_spv_ffi_init_logging(level_c.as_ptr(), true, std::ptr::null(), 0);
125125

126126
// Build config
127127
let cfg = dash_spv_ffi_config_new(network);

dash-spv-ffi/src/error.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ impl From<SpvError> for FFIErrorCode {
6060
SpvError::Io(_) => FFIErrorCode::RuntimeError,
6161
SpvError::Config(_) => FFIErrorCode::ConfigError,
6262
SpvError::Parse(_) => FFIErrorCode::ValidationError,
63+
SpvError::Logging(_) => FFIErrorCode::RuntimeError,
6364
SpvError::Wallet(_) => FFIErrorCode::WalletError,
6465
SpvError::QuorumLookupError(_) => FFIErrorCode::ValidationError,
6566
SpvError::General(_) => FFIErrorCode::Unknown,

dash-spv-ffi/src/utils.rs

Lines changed: 65 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,84 @@
1-
use crate::{set_last_error, FFIErrorCode};
21
use std::ffi::CStr;
32
use std::os::raw::c_char;
3+
use std::path::PathBuf;
4+
use std::sync::OnceLock;
5+
6+
use crate::{set_last_error, FFIErrorCode};
7+
use dash_spv::{LogFileConfig, LoggingConfig};
8+
9+
/// Static storage for the logging guard to keep it alive for the FFI lifetime.
10+
/// The guard must remain alive for log flushing to work correctly.
11+
static LOGGING_GUARD: OnceLock<dash_spv::LoggingGuard> = OnceLock::new();
412

513
/// Initialize logging for the SPV library.
614
///
15+
/// # Arguments
16+
/// - `level`: Log level string (null uses RUST_LOG env var or defaults to INFO).
17+
/// Valid values: "error", "warn", "info", "debug", "trace"
18+
/// - `enable_console`: Whether to output logs to console (stderr)
19+
/// - `log_dir`: Directory for log files (null to disable file logging)
20+
/// - `max_files`: Maximum archived log files to retain (ignored if log_dir is null)
21+
///
722
/// # Safety
8-
/// - `level` may be null or point to a valid, NUL-terminated C string.
9-
/// - If non-null, the pointer must remain valid for the duration of this call.
23+
/// - `level` and `log_dir` may be null or point to valid, NUL-terminated C strings.
1024
#[no_mangle]
11-
pub unsafe extern "C" fn dash_spv_ffi_init_logging(level: *const c_char) -> i32 {
12-
let level_str = if level.is_null() {
13-
"info"
25+
pub unsafe extern "C" fn dash_spv_ffi_init_logging(
26+
level: *const c_char,
27+
enable_console: bool,
28+
log_dir: *const c_char,
29+
max_files: usize,
30+
) -> i32 {
31+
let level_filter = if level.is_null() {
32+
None
1433
} else {
1534
match CStr::from_ptr(level).to_str() {
16-
Ok(s) => s,
35+
Ok(s) => match s.parse() {
36+
Ok(lf) => Some(lf),
37+
Err(_) => {
38+
set_last_error(&format!(
39+
"Invalid log level '{}'. Valid: error, warn, info, debug, trace",
40+
s
41+
));
42+
return FFIErrorCode::InvalidArgument as i32;
43+
}
44+
},
1745
Err(e) => {
1846
set_last_error(&format!("Invalid UTF-8 in log level: {}", e));
1947
return FFIErrorCode::InvalidArgument as i32;
2048
}
2149
}
2250
};
2351

24-
match dash_spv::init_logging(level_str) {
25-
Ok(()) => FFIErrorCode::Success as i32,
52+
let file_config = if log_dir.is_null() {
53+
None
54+
} else {
55+
match CStr::from_ptr(log_dir).to_str() {
56+
Ok(s) => Some(LogFileConfig {
57+
log_dir: PathBuf::from(s),
58+
max_files,
59+
}),
60+
Err(e) => {
61+
set_last_error(&format!("Invalid UTF-8 in log directory: {}", e));
62+
return FFIErrorCode::InvalidArgument as i32;
63+
}
64+
}
65+
};
66+
67+
let config = LoggingConfig {
68+
level: level_filter,
69+
console: enable_console,
70+
file: file_config,
71+
};
72+
73+
match dash_spv::init_logging(config) {
74+
Ok(guard) => {
75+
// Store guard in static to keep it alive for log flushing.
76+
// OnceLock::set returns Err if already set (first init wins).
77+
if LOGGING_GUARD.set(guard).is_err() {
78+
tracing::warn!("Logging already initialized, ignoring subsequent init");
79+
}
80+
FFIErrorCode::Success as i32
81+
}
2682
Err(e) => {
2783
set_last_error(&format!("Failed to initialize logging: {}", e));
2884
FFIErrorCode::RuntimeError as i32

dash-spv-ffi/tests/test_event_callbacks.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ extern "C" fn test_balance_callback(confirmed: u64, unconfirmed: u64, user_data:
138138
fn test_event_callbacks_setup() {
139139
// Initialize logging
140140
unsafe {
141-
dash_spv_ffi_init_logging(c"debug".as_ptr());
141+
dash_spv_ffi_init_logging(c"debug".as_ptr(), true, std::ptr::null(), 0);
142142
}
143143

144144
// Create test data
@@ -238,7 +238,7 @@ fn test_event_callbacks_setup() {
238238
#[serial]
239239
fn test_enhanced_event_callbacks() {
240240
unsafe {
241-
dash_spv_ffi_init_logging(c"info".as_ptr());
241+
dash_spv_ffi_init_logging(c"info".as_ptr(), true, std::ptr::null(), 0);
242242

243243
// Create test data
244244
let event_data = TestEventData::new();

dash-spv-ffi/tests/test_utils.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,15 @@ mod tests {
1111
fn test_init_logging() {
1212
unsafe {
1313
let level = CString::new("debug").unwrap();
14-
let result = dash_spv_ffi_init_logging(level.as_ptr());
14+
let result = dash_spv_ffi_init_logging(level.as_ptr(), true, std::ptr::null(), 0);
1515
// May fail if already initialized, but should handle gracefully
1616
assert!(
1717
result == FFIErrorCode::Success as i32
1818
|| result == FFIErrorCode::RuntimeError as i32
1919
);
2020

21-
// Test with null pointer (should use default)
22-
let result = dash_spv_ffi_init_logging(std::ptr::null());
21+
// Test with null level pointer (should use RUST_LOG or default to INFO)
22+
let result = dash_spv_ffi_init_logging(std::ptr::null(), true, std::ptr::null(), 0);
2323
assert!(
2424
result == FFIErrorCode::Success as i32
2525
|| result == FFIErrorCode::RuntimeError as i32

dash-spv/Cargo.toml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ key-wallet-manager = { path = "../key-wallet-manager" }
1919
blsful = { git = "https://github.com/dashpay/agora-blsful", rev = "0c34a7a488a0bd1c9a9a2196e793b303ad35c900" }
2020

2121
# CLI
22-
clap = { version = "4.0", features = ["derive"] }
22+
clap = { version = "4.0", features = ["derive", "env"] }
2323

2424
# Async runtime
2525
tokio = { version = "1.0", features = ["full"] }
@@ -37,7 +37,9 @@ bincode = "1.3"
3737

3838
# Logging
3939
tracing = "0.1"
40-
tracing-subscriber = "0.3"
40+
tracing-subscriber = { version = "0.3.20", features = ["env-filter"] }
41+
tracing-appender = "0.2"
42+
chrono = "0.4.20"
4143

4244
# Utilities
4345
rand = "0.8"

0 commit comments

Comments
 (0)