Skip to content

Commit 82b9211

Browse files
committed
Add a logger for VSS
This is a clone of the `ldk-server` logger
1 parent b96325a commit 82b9211

6 files changed

Lines changed: 270 additions & 11 deletions

File tree

rust/server/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,5 @@ prost = { version = "0.11.6", default-features = false, features = ["std"] }
2121
bytes = "1.4.0"
2222
serde = { version = "1.0.203", default-features = false, features = ["derive"] }
2323
toml = { version = "0.8.9", default-features = false, features = ["parse"] }
24+
log = { version = "0.4.29", default-features = false, features = ["std"] }
25+
chrono = { version = "0.4", default-features = false, features = ["clock"] }

rust/server/src/main.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,16 @@ use tokio::signal::unix::SignalKind;
1717
use hyper::server::conn::http1;
1818
use hyper_util::rt::TokioIo;
1919

20+
use log::error;
21+
2022
use api::auth::{Authorizer, NoopAuthorizer};
2123
use api::kv_store::KvStore;
2224
#[cfg(feature = "jwt")]
2325
use auth_impls::jwt::JWTAuthorizer;
2426
#[cfg(feature = "sigs")]
2527
use auth_impls::signature::SignatureValidatingAuthorizer;
2628
use impls::postgres_store::{PostgresPlaintextBackend, PostgresTlsBackend};
29+
use util::logger::ServerLogger;
2730
use vss_service::VssService;
2831

2932
mod util;
@@ -38,6 +41,14 @@ fn main() {
3841
std::process::exit(-1);
3942
});
4043

44+
let logger = match ServerLogger::init(config.log_level, &config.log_file) {
45+
Ok(logger) => logger,
46+
Err(e) => {
47+
eprintln!("Failed to initialize logger: {e}");
48+
std::process::exit(-1);
49+
},
50+
};
51+
4152
let runtime = match tokio::runtime::Builder::new_multi_thread().enable_all().build() {
4253
Ok(runtime) => Arc::new(runtime),
4354
Err(e) => {
@@ -47,6 +58,15 @@ fn main() {
4758
};
4859

4960
runtime.block_on(async {
61+
// Register SIGHUP handler for log rotation
62+
let mut sighup_stream = match tokio::signal::unix::signal(SignalKind::hangup()) {
63+
Ok(stream) => stream,
64+
Err(e) => {
65+
eprintln!("Failed to register SIGHUP handler: {e}");
66+
std::process::exit(-1);
67+
}
68+
};
69+
5070
let mut sigterm_stream = match tokio::signal::unix::signal(SignalKind::terminate()) {
5171
Ok(stream) => stream,
5272
Err(e) => {
@@ -146,6 +166,11 @@ fn main() {
146166
println!("Received CTRL-C, shutting down..");
147167
break;
148168
}
169+
_ = sighup_stream.recv() => {
170+
if let Err(e) = logger.reopen() {
171+
error!("Failed to reopen log file on SIGHUP: {e}");
172+
}
173+
}
149174
_ = sigterm_stream.recv() => {
150175
println!("Received SIGTERM, shutting down..");
151176
break;

rust/server/src/util/config.rs

Lines changed: 61 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
1+
use log::LevelFilter;
12
use serde::Deserialize;
23
use std::net::SocketAddr;
4+
use std::path::PathBuf;
35

46
const BIND_ADDR_VAR: &str = "VSS_BIND_ADDRESS";
7+
const LOG_FILE_VAR: &str = "VSS_LOG_FILE";
8+
const LOG_LEVEL_VAR: &str = "VSS_LOG_LEVEL";
59
const JWT_RSA_PEM_VAR: &str = "VSS_JWT_RSA_PEM";
610
const PSQL_USER_VAR: &str = "VSS_PSQL_USERNAME";
711
const PSQL_PASS_VAR: &str = "VSS_PSQL_PASSWORD";
@@ -16,6 +20,7 @@ const PSQL_CERT_PEM_VAR: &str = "VSS_PSQL_CRT_PEM";
1620
#[derive(Deserialize, Default)]
1721
struct TomlConfig {
1822
server_config: Option<ServerConfig>,
23+
log_config: Option<LogConfig>,
1924
jwt_auth_config: Option<JwtAuthConfig>,
2025
postgresql_config: Option<PostgreSQLConfig>,
2126
}
@@ -45,6 +50,12 @@ struct TlsConfig {
4550
crt_pem: Option<String>,
4651
}
4752

53+
#[derive(Deserialize)]
54+
struct LogConfig {
55+
level: Option<String>,
56+
file: Option<PathBuf>,
57+
}
58+
4859
// Encapsulates the result of reading both the environment variables and the config file.
4960
pub(crate) struct Configuration {
5061
pub(crate) bind_address: SocketAddr,
@@ -53,6 +64,8 @@ pub(crate) struct Configuration {
5364
pub(crate) default_db: String,
5465
pub(crate) vss_db: String,
5566
pub(crate) tls_config: Option<Option<String>>,
67+
pub(crate) log_file: PathBuf,
68+
pub(crate) log_level: LevelFilter,
5669
}
5770

5871
#[inline]
@@ -75,15 +88,16 @@ fn read_config<'a, T: std::fmt::Display>(
7588
}
7689

7790
pub(crate) fn load_configuration(config_file_path: Option<&str>) -> Result<Configuration, String> {
78-
let TomlConfig { server_config, jwt_auth_config, postgresql_config } = match config_file_path {
79-
Some(path) => {
80-
let config_file = std::fs::read_to_string(path)
81-
.map_err(|e| format!("Failed to read configuration file: {}", e))?;
82-
toml::from_str(&config_file)
83-
.map_err(|e| format!("Failed to parse configuration file: {}", e))?
84-
},
85-
None => TomlConfig::default(), // All fields are set to `None`
86-
};
91+
let TomlConfig { server_config, log_config, jwt_auth_config, postgresql_config } =
92+
match config_file_path {
93+
Some(path) => {
94+
let config_file = std::fs::read_to_string(path)
95+
.map_err(|e| format!("Failed to read configuration file: {}", e))?;
96+
toml::from_str(&config_file)
97+
.map_err(|e| format!("Failed to parse configuration file: {}", e))?
98+
},
99+
None => TomlConfig::default(), // All fields are set to `None`
100+
};
87101

88102
let bind_address_env = read_env(BIND_ADDR_VAR)?
89103
.map(|addr| {
@@ -99,6 +113,34 @@ pub(crate) fn load_configuration(config_file_path: Option<&str>) -> Result<Confi
99113
BIND_ADDR_VAR,
100114
)?;
101115

116+
let log_level_env: Option<LevelFilter> = read_env(LOG_LEVEL_VAR)?
117+
.map(|level_str| {
118+
level_str
119+
.parse()
120+
.map_err(|e| format!("Unable to parse the log level environment variable: {}", e))
121+
})
122+
.transpose()?;
123+
let log_level_config: Option<LevelFilter> = log_config
124+
.as_ref()
125+
.and_then(|config| config.level.as_ref())
126+
.map(|level_str| {
127+
level_str
128+
.parse()
129+
.map_err(|e| format!("Unable to parse the log level config variable: {}", e))
130+
})
131+
.transpose()?;
132+
let log_level = log_level_env.or(log_level_config).unwrap_or(LevelFilter::Debug);
133+
134+
let log_file_env: Option<PathBuf> = read_env(LOG_FILE_VAR)?
135+
.map(|file_str| {
136+
file_str
137+
.parse()
138+
.map_err(|e| format!("Unable to parse the log file environment variable: {}", e))
139+
})
140+
.transpose()?;
141+
let log_file_config: Option<PathBuf> = log_config.and_then(|config| config.file);
142+
let log_file = log_file_env.or(log_file_config).unwrap_or(PathBuf::from("vss.log"));
143+
102144
let rsa_pem_env = read_env(JWT_RSA_PEM_VAR)?;
103145
let rsa_pem = rsa_pem_env.or(jwt_auth_config.and_then(|config| config.rsa_pem));
104146

@@ -155,5 +197,14 @@ pub(crate) fn load_configuration(config_file_path: Option<&str>) -> Result<Confi
155197

156198
let postgresql_prefix = format!("postgresql://{}:{}@{}", username, password, address);
157199

158-
Ok(Configuration { bind_address, rsa_pem, postgresql_prefix, default_db, vss_db, tls_config })
200+
Ok(Configuration {
201+
bind_address,
202+
log_file,
203+
log_level,
204+
rsa_pem,
205+
postgresql_prefix,
206+
default_db,
207+
vss_db,
208+
tls_config,
209+
})
159210
}

rust/server/src/util/logger.rs

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
// This file is Copyright its original authors, visible in version control
2+
// history.
3+
//
4+
// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
5+
// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
6+
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
7+
// You may not use this file except in accordance with one or both of these
8+
// licenses.
9+
10+
use std::fs::{self, File, OpenOptions};
11+
use std::io::{self, Write};
12+
use std::path::{Path, PathBuf};
13+
use std::sync::{Arc, Mutex};
14+
15+
use log::{Level, LevelFilter, Log, Metadata, Record};
16+
17+
/// A logger implementation that writes logs to both stderr and a file.
18+
///
19+
/// The logger formats log messages with RFC3339 timestamps and writes them to:
20+
/// - stdout/stderr for console output
21+
/// - A file specified during initialization
22+
///
23+
/// All log messages follow the format:
24+
/// `[TIMESTAMP LEVEL TARGET FILE:LINE] MESSAGE`
25+
///
26+
/// Example: `[2025-12-04T10:30:45Z INFO ldk_server:42] Starting up...`
27+
///
28+
/// The logger handles SIGHUP for log rotation by reopening the file handle when signaled.
29+
pub struct ServerLogger {
30+
/// The maximum log level to display
31+
level: LevelFilter,
32+
/// The file to write logs to, protected by a mutex for thread-safe access
33+
file: Mutex<File>,
34+
/// Path to the log file for reopening on SIGHUP
35+
log_file_path: PathBuf,
36+
}
37+
38+
impl ServerLogger {
39+
/// Initializes the global logger with the specified level and file path.
40+
///
41+
/// Opens or creates the log file at the given path. If the file exists, logs are appended.
42+
/// If the file doesn't exist, it will be created along with any necessary parent directories.
43+
///
44+
/// This should be called once at application startup. Subsequent calls will fail.
45+
///
46+
/// Returns an Arc to the logger for signal handling purposes.
47+
pub fn init(level: LevelFilter, log_file_path: &Path) -> Result<Arc<Self>, io::Error> {
48+
// Create parent directories if they don't exist
49+
if let Some(parent) = log_file_path.parent() {
50+
fs::create_dir_all(parent)?;
51+
}
52+
53+
let file = open_log_file(log_file_path)?;
54+
55+
let logger = Arc::new(ServerLogger {
56+
level,
57+
file: Mutex::new(file),
58+
log_file_path: log_file_path.to_path_buf(),
59+
});
60+
61+
log::set_boxed_logger(Box::new(LoggerWrapper(Arc::clone(&logger))))
62+
.map_err(io::Error::other)?;
63+
log::set_max_level(level);
64+
Ok(logger)
65+
}
66+
67+
/// Reopens the log file. Called on SIGHUP for log rotation.
68+
pub fn reopen(&self) -> Result<(), io::Error> {
69+
let new_file = open_log_file(&self.log_file_path)?;
70+
match self.file.lock() {
71+
Ok(mut file) => {
72+
// Flush the old buffer before replacing with the new file
73+
file.flush()?;
74+
*file = new_file;
75+
Ok(())
76+
},
77+
Err(e) => Err(io::Error::other(format!("Failed to acquire lock: {e}"))),
78+
}
79+
}
80+
}
81+
82+
impl Log for ServerLogger {
83+
fn enabled(&self, metadata: &Metadata) -> bool {
84+
metadata.level() <= self.level
85+
}
86+
87+
fn log(&self, record: &Record) {
88+
if self.enabled(record.metadata()) {
89+
let level_str = format_level(record.level());
90+
let line = record.line().unwrap_or(0);
91+
92+
// Log to console
93+
let _ = match record.level() {
94+
Level::Error => {
95+
writeln!(
96+
io::stderr(),
97+
"[{} {} {}:{}] {}",
98+
format_timestamp(),
99+
level_str,
100+
record.target(),
101+
line,
102+
record.args()
103+
)
104+
},
105+
_ => {
106+
writeln!(
107+
io::stdout(),
108+
"[{} {} {}:{}] {}",
109+
format_timestamp(),
110+
level_str,
111+
record.target(),
112+
line,
113+
record.args()
114+
)
115+
},
116+
};
117+
118+
// Log to file
119+
if let Ok(mut file) = self.file.lock() {
120+
let _ = writeln!(
121+
file,
122+
"[{} {} {}:{}] {}",
123+
format_timestamp(),
124+
level_str,
125+
record.target(),
126+
line,
127+
record.args()
128+
);
129+
}
130+
}
131+
}
132+
133+
fn flush(&self) {
134+
let _ = io::stdout().flush();
135+
let _ = io::stderr().flush();
136+
if let Ok(mut file) = self.file.lock() {
137+
let _ = file.flush();
138+
}
139+
}
140+
}
141+
142+
fn format_timestamp() -> String {
143+
let now = chrono::Utc::now();
144+
now.to_rfc3339_opts(chrono::SecondsFormat::Millis, true)
145+
}
146+
147+
fn format_level(level: Level) -> &'static str {
148+
match level {
149+
Level::Error => "ERROR",
150+
Level::Warn => "WARN ",
151+
Level::Info => "INFO ",
152+
Level::Debug => "DEBUG",
153+
Level::Trace => "TRACE",
154+
}
155+
}
156+
157+
fn open_log_file(log_file_path: &Path) -> Result<File, io::Error> {
158+
OpenOptions::new().create(true).append(true).open(log_file_path)
159+
}
160+
161+
/// Wrapper to allow Arc<ServerLogger> to implement Log trait
162+
struct LoggerWrapper(Arc<ServerLogger>);
163+
164+
impl Log for LoggerWrapper {
165+
fn enabled(&self, metadata: &Metadata) -> bool {
166+
self.0.enabled(metadata)
167+
}
168+
169+
fn log(&self, record: &Record) {
170+
self.0.log(record)
171+
}
172+
173+
fn flush(&self) {
174+
self.0.flush()
175+
}
176+
}

rust/server/src/util/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
pub(crate) mod config;
2+
pub(crate) mod logger;

rust/server/vss-server-config.toml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,13 @@ vss_database = "vss" # Optional in TOML, can be overridden by env var
1919

2020
# [postgresql_config.tls] # Uncomment, or set env var `VSS_PSQL_TLS` to make TLS connections to the postgres database
2121
#
22-
# Uncomment the lines below, or set `VSS_PSQL_CRT_PEM`, to add a root certificate to your trusted root certificates
22+
# Uncomment the lines below, or set `VSS_PSQL_CRT_PEM` to add a root certificate to your trusted root certificates
2323
#
2424
# crt_pem = """
2525
# -----BEGIN CERTIFICATE-----
2626
# -----END CERTIFICATE-----
2727
# """
28+
29+
# [log_config]
30+
# level = "debug" # Uncomment, or set env var `VSS_LOG_LEVEL` to set the log level, the default is "debug"
31+
# file = "vss.log" # Uncomment, or set env var `VSS_LOG_FILE` to set the log file path, the default is "vss.log"

0 commit comments

Comments
 (0)