Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions src/bin/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use crate::errors::BinaryError;
use clap::Parser;
use ferrumc_config::whitelist::create_whitelist;
use ferrumc_world::pos::ChunkPos;
use std::process::ExitCode;
use std::sync::Arc;
use std::time::Instant;
use tracing::{error, info};
Expand All @@ -28,14 +29,18 @@ static ALLOC: dhat::Alloc = dhat::Alloc;
static GLOBAL: tracy_client::ProfiledAllocator<std::alloc::System> =
tracy_client::ProfiledAllocator::new(std::alloc::System, 100);

fn main() {
fn main() -> ExitCode {
#[cfg(feature = "dhat")]
let _profiler = dhat::Profiler::new_heap();

let start_time = Instant::now();

let cli_args = CLIArgs::parse();
ferrumc_logging::init_logging(cli_args.log.into());
if let Err(err) = ferrumc_logging::init_logging(cli_args.log.into()) {
// Tracing is not available yet; print a plain message and exit cleanly.
eprintln!("error: {err}");
return ExitCode::FAILURE;
}

ferrumc_registry::init();

Expand Down Expand Up @@ -78,6 +83,8 @@ fn main() {
}
}
}

ExitCode::SUCCESS
}

fn entry(start_time: Instant) -> Result<(), BinaryError> {
Expand Down
3 changes: 3 additions & 0 deletions src/lib/utils/logging/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ thiserror = { workspace = true }
tracing-tracy = { workspace = true, optional = true }
tracy-client = { workspace = true, optional = true }

[dev-dependencies]
tempfile = { workspace = true }

[features]
# Enable the tracy profiler client
tracy = ["tracing-tracy", "tracy-client"]
34 changes: 31 additions & 3 deletions src/lib/utils/logging/src/errors.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,35 @@
use std::io;
use thiserror::Error;

#[derive(Debug, Clone, Error)]
#[derive(Debug, Error)]
pub enum LoggingError {
#[error("Something failed lol")]
SomeError,
/// Rolling file appender could not create or open the log directory.
#[error(
"failed to create log directory at `{path}`: {source}\n\
Ensure the process has permission to create and write files in that directory."
)]
LogDirectory {
path: String,
#[source]
source: io::Error,
},
}

impl LoggingError {
/// Builds a [`LoggingError::LogDirectory`] from a tracing-appender init failure.
pub(crate) fn from_appender_init(
path: impl AsRef<std::path::Path>,
err: tracing_appender::rolling::InitError,
) -> Self {
use std::error::Error as _;

let path = path.as_ref().display().to_string();
let source = err
.source()
.and_then(|source| source.downcast_ref::<io::Error>())
.map(|io_err| io::Error::new(io_err.kind(), io_err.to_string()))
.unwrap_or_else(|| io::Error::other(err.to_string()));

Self::LogDirectory { path, source }
}
}
112 changes: 102 additions & 10 deletions src/lib/utils/logging/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,32 +1,63 @@
pub mod errors;

use crate::errors::LoggingError;
use ferrumc_general_purpose::paths::get_root_path;
use ferrumc_profiling::ProfilerTracingLayer;
use std::path::Path;
use tracing::Level;
use tracing_appender::rolling::Rotation;
use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::util::SubscriberInitExt;
use tracing_subscriber::EnvFilter;

pub fn init_logging(trace_level: Level) {
//let console = console_subscriber::spawn();
/// Initializes console and rolling-file logging under `{root}/logs`.
///
/// Returns [`LoggingError`] when the log directory cannot be created or opened
/// (for example, permission denied), instead of panicking.
pub fn init_logging(trace_level: Level) -> Result<(), LoggingError> {
init_logging_to(trace_level, get_root_path().join("logs"))
}

/// Same as [`init_logging`], but writes under `logs_dir` (used by tests).
pub(crate) fn init_logging_to(
trace_level: Level,
logs_dir: impl AsRef<Path>,
) -> Result<(), LoggingError> {
let logs_dir = logs_dir.as_ref();

let env_filter = EnvFilter::builder()
.with_default_directive(trace_level.into())
.parse_lossy("");

// Disallow request and hyper-util debug prints since they spam the console
let env_filter = env_filter
.add_directive("reqwest=warn".parse().unwrap())
.add_directive("h2=warn".parse().unwrap())
.add_directive("rustls=warn".parse().unwrap())
.add_directive("hyper_util=warn".parse().unwrap());
.add_directive(
"reqwest=warn"
.parse()
.expect("static filter directive `reqwest=warn` must parse"),
)
.add_directive(
"h2=warn"
.parse()
.expect("static filter directive `h2=warn` must parse"),
)
.add_directive(
"rustls=warn"
.parse()
.expect("static filter directive `rustls=warn` must parse"),
)
.add_directive(
"hyper_util=warn"
.parse()
.expect("static filter directive `hyper_util=warn` must parse"),
);

let file_appender = tracing_appender::rolling::Builder::new()
.rotation(Rotation::DAILY)
.filename_prefix("ferrumc")
.filename_suffix("log.txt")
.build(get_root_path().join("logs"))
.unwrap();
.build(logs_dir)
.map_err(|err| LoggingError::from_appender_init(logs_dir, err))?;

let fmt_layer = {
#[cfg(debug_assertions)]
Expand Down Expand Up @@ -69,9 +100,70 @@ pub fn init_logging(trace_level: Level) {
#[cfg(feature = "tracy")]
{
let tracy_layer = tracing_tracy::TracyLayer::default();
// Registry becomes a different type when a layer is added, so we need to
// shadow it here and initialize it separately.
// Registry becomes a different type when a layer is added, so it is
// shadowed here and initialized separately.
let registry = registry.with(tracy_layer);
registry.init();
}

Ok(())
}

#[cfg(test)]
mod tests {
use super::*;
use std::io::ErrorKind;

#[cfg(unix)]
use std::fs;
#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;
#[cfg(unix)]
use std::path::PathBuf;

#[test]
fn log_directory_error_message_is_actionable() {
let err = LoggingError::LogDirectory {
path: "/unwritable/logs".to_string(),
source: std::io::Error::new(ErrorKind::PermissionDenied, "Permission denied"),
};
let message = err.to_string();
assert!(
message.contains("failed to create log directory at `/unwritable/logs`"),
"message missing path context: {message}"
);
assert!(
message.contains("Permission denied"),
"message missing OS reason: {message}"
);
assert!(
message.contains("permission to create and write"),
"message missing remediation hint: {message}"
);
}

#[test]
#[cfg(unix)]
fn init_logging_to_unwritable_dir_returns_error() {
let temp = tempfile::tempdir().expect("tempdir");
let logs_dir: PathBuf = temp.path().join("logs");
fs::create_dir(&logs_dir).expect("create logs dir");

let mut perms = fs::metadata(&logs_dir).expect("metadata").permissions();
// Owner read+execute only — create_dir/open for append should fail.
perms.set_mode(0o500);
fs::set_permissions(&logs_dir, perms).expect("chmod");

let result = init_logging_to(Level::INFO, &logs_dir);

// Restore writable so TempDir cleanup succeeds.
let mut perms = fs::metadata(&logs_dir).expect("metadata").permissions();
perms.set_mode(0o700);
fs::set_permissions(&logs_dir, perms).expect("chmod restore");

let err = result.expect_err("expected permission failure");
let LoggingError::LogDirectory { path, source } = err;
assert_eq!(path, logs_dir.display().to_string());
assert_eq!(source.kind(), ErrorKind::PermissionDenied);
}
}
Loading