Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add random 6-character suffix to log file names #1041

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
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
3 changes: 3 additions & 0 deletions doc/LOGGING.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ You can direct logs to a file instead of syslog by providing a destination direc

The directory will be created if it doesn't exist.
A new log file will be created for each execution of `mount-s3`.

Log file names are not considered stable and may change in the future.

Both the directory and log files are created with read/write access for the process owner and read access for the process owner's group.
Log files are not automatically rotated or cleaned up.

Expand Down
6 changes: 6 additions & 0 deletions mountpoint-s3/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
## Unreleased

### Breaking changes

* When configured to log to a directory, Mountpoint now includes a random string following the timestamp in the file name.
Previously, multiple Mountpoint processes would write to the same log file causing log entries to be interleaved.
([#1041](https://github.com/awslabs/mountpoint-s3/pull/1041))

## v1.9.1 (September 19, 2024)

### Other changes
Expand Down
1 change: 1 addition & 0 deletions mountpoint-s3/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ linked-hash-map = "0.5.6"
metrics = "0.22.1"
nix = { version = "0.29.0", default-features = false, features = ["fs", "process", "signal", "user"] }
owo-colors = { version = "4.0.0", features = ["supports-colors"] }
rand = "0.8.5"
regex = "1.7.1"
serde = { version = "1.0.190", features = ["derive"] }
serde_json = "1.0.95"
Expand Down
69 changes: 38 additions & 31 deletions mountpoint-s3/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ use crate::data_cache::{CacheLimit, DiskDataCache, DiskDataCacheConfig, ExpressD
use crate::fs::{CacheConfig, S3FilesystemConfig, ServerSideEncryption, TimeToLive};
use crate::fuse::session::FuseSession;
use crate::fuse::S3FuseFilesystem;
use crate::logging::{init_logging, LoggingConfig};
use crate::logging::{init_logging, prepare_log_file_name, LoggingConfig};
use crate::mem_limiter::MINIMUM_MEM_LIMIT;
use crate::prefetch::{caching_prefetch, default_prefetch, Prefetch};
use crate::prefix::Prefix;
Expand Down Expand Up @@ -438,30 +438,6 @@ impl CliArgs {
None
}

fn logging_config(&self) -> LoggingConfig {
let default_filter = if self.no_log {
String::from("off")
} else {
let mut filter = if self.debug {
String::from("debug")
} else {
String::from("warn")
};
let crt_verbosity = if self.debug_crt { "debug" } else { "off" };
filter.push_str(&format!(",{}={}", AWSCRT_LOG_TARGET, crt_verbosity));
if self.log_metrics {
filter.push_str(&format!(",{}=info", metrics::TARGET_NAME));
}
filter
};

LoggingConfig {
log_directory: self.log_directory.clone(),
log_to_stdout: self.foreground,
default_filter,
}
}

/// Human-readable description of the bucket being mounted
fn bucket_description(&self) -> String {
if let Some(prefix) = self.prefix.as_ref() {
Expand Down Expand Up @@ -501,6 +477,36 @@ impl CliArgs {
}
}

/// Generates a logging configuration based on the CLI arguments.
///
/// This includes random string generation which can change with each invocation,
/// so once created the [LoggingConfig] should be cloned if another owned copy is required.
fn logging_config(cli_args: &CliArgs) -> LoggingConfig {
let default_filter = if cli_args.no_log {
String::from("off")
} else {
let mut filter = if cli_args.debug {
String::from("debug")
} else {
String::from("warn")
};
let crt_verbosity = if cli_args.debug_crt { "debug" } else { "off" };
filter.push_str(&format!(",{}={}", AWSCRT_LOG_TARGET, crt_verbosity));
if cli_args.log_metrics {
filter.push_str(&format!(",{}=info", metrics::TARGET_NAME));
}
filter
};

let log_file = cli_args.log_directory.as_ref().map(|dir| prepare_log_file_name(dir));

LoggingConfig {
log_file,
log_to_stdout: cli_args.foreground,
default_filter,
}
}

Comment on lines +480 to +509
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've moved this out of the impl CliArgs because it produces a different return value each time (due to randomness). I hope moving it out makes this clearer (along with RustDoc).

pub fn main<ClientBuilder, Client, Runtime>(client_builder: ClientBuilder) -> anyhow::Result<()>
where
ClientBuilder: FnOnce(&CliArgs) -> anyhow::Result<(Client, Runtime, S3Personality)>,
Expand All @@ -515,7 +521,7 @@ where
);

if args.foreground {
init_logging(args.logging_config()).context("failed to initialize logging")?;
init_logging(logging_config(&args)).context("failed to initialize logging")?;

let _metrics = metrics::install();

Expand All @@ -532,17 +538,20 @@ where
// child process will report its status via this pipe.
let (read_fd, write_fd) = nix::unistd::pipe().context("Failed to create a pipe")?;

// Prepare logging configuration up front, so the values are shared between the two processes.
let logging_config = logging_config(&args);

// Don't share args across the fork. It should just be plain data, so probably fine to be
// copy-on-write, but just in case we ever add something more fancy to the struct.
drop(args);

// SAFETY: Child process has full ownership of its resources.
// There is no shared data between parent and child processes.
// There is no shared data between parent and child processes other than logging configuration.
let pid = unsafe { nix::unistd::fork() };
match pid.expect("Failed to fork mount process") {
ForkResult::Child => {
let args = CliArgs::parse();
init_logging(args.logging_config()).context("failed to initialize logging")?;
init_logging(logging_config).context("failed to initialize logging")?;

let _metrics = metrics::install();

Expand Down Expand Up @@ -584,9 +593,7 @@ where
}
}
ForkResult::Parent { child } => {
let args = CliArgs::parse();

init_logging(args.logging_config()).context("failed to initialize logging")?;
init_logging(logging_config).context("failed to initialize logging")?;

// close unused file descriptor, we only read from this end.
drop(write_fd);
Expand Down
53 changes: 36 additions & 17 deletions mountpoint-s3/src/logging.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@ use std::fs::{DirBuilder, OpenOptions};
use std::os::unix::fs::DirBuilderExt;
use std::os::unix::prelude::OpenOptionsExt;
use std::panic::{self, PanicInfo};
use std::path::PathBuf;
use std::path::{Path, PathBuf};
use std::thread;

use crate::metrics::metrics_tracing_span_layer;
use anyhow::Context;
use mountpoint_s3_crt::common::rust_log_adapter::RustLogAdapter;
use rand::Rng;
use time::format_description::FormatItem;
use time::macros;
use time::OffsetDateTime;
Expand All @@ -21,11 +22,13 @@ use tracing_subscriber::{Layer, Registry};
mod syslog;
use self::syslog::SyslogLayer;

/// Configuration for Mountpoint logging
/// Configuration for Mountpoint logging.
///
/// This configuration struct is safe to use across forks.
#[derive(Debug)]
pub struct LoggingConfig {
/// A directory to create log files in. If unspecified, logs will be routed to syslog.
pub log_directory: Option<PathBuf>,
/// File to write logs into. If unspecified, logs will be routed to syslog.
pub log_file: Option<PathBuf>,
/// Whether to duplicate logs to stdout in addition to syslog or the log directory.
pub log_to_stdout: bool,
/// The default filter directive (in the sense of [tracing_subscriber::filter::EnvFilter]) to
Expand All @@ -45,6 +48,28 @@ pub fn init_logging(config: LoggingConfig) -> anyhow::Result<()> {
Ok(())
}

/// For a given log directory, prepare a file name for this Mountpoint.
///
/// This may include a randomly generated component and return different results between invocations.
pub fn prepare_log_file_name(log_directory: &Path) -> PathBuf {
let timestamp = {
const TIMESTAMP_FORMAT: &[FormatItem<'static>] =
macros::format_description!("[year]-[month]-[day]T[hour]-[minute]-[second]Z");
OffsetDateTime::now_utc()
.format(TIMESTAMP_FORMAT)
.expect("couldn't format timestamp for log file name")
};

let random_suffix: String = rand::thread_rng()
.sample_iter(&rand::distributions::Alphanumeric)
.take(6)
.map(char::from)
.collect();
let file_name = format!("mountpoint-s3-{timestamp}.{random_suffix}.log");

log_directory.join(file_name)
}

fn tracing_panic_hook(panic_info: &PanicInfo) {
let location = panic_info
.location()
Expand Down Expand Up @@ -92,23 +117,17 @@ fn init_tracing_subscriber(config: LoggingConfig) -> anyhow::Result<()> {

RustLogAdapter::try_init().context("failed to initialize CRT logger")?;

let file_layer = if let Some(path) = &config.log_directory {
const LOG_FILE_NAME_FORMAT: &[FormatItem<'static>] =
macros::format_description!("mountpoint-s3-[year]-[month]-[day]T[hour]-[minute]-[second]Z.log");
let filename = OffsetDateTime::now_utc()
.format(LOG_FILE_NAME_FORMAT)
.context("couldn't format log file name")?;

// log directories and files created by Mountpoint should not be accessible by other users
let file_layer = if let Some(log_file_path) = &config.log_file {
// log directories and files created by Mountpoint should not be writable by other users
let mut dir_builder = DirBuilder::new();
dir_builder.recursive(true).mode(0o750);
let mut file_options = OpenOptions::new();
file_options.mode(0o640).append(true).create(true);

dir_builder.create(path).context("failed to create log folder")?;
let file = file_options
.open(path.join(filename))
.context("failed to create log file")?;
if let Some(parent_dir) = log_file_path.parent() {
dir_builder.create(parent_dir).context("failed to create log folder")?;
}
let file = file_options.open(log_file_path).context("failed to create log file")?;

let file_layer = tracing_subscriber::fmt::layer()
.with_ansi(false)
Expand All @@ -119,7 +138,7 @@ fn init_tracing_subscriber(config: LoggingConfig) -> anyhow::Result<()> {
None
};

let syslog_layer: Option<Filtered<_, _, Registry>> = if config.log_directory.is_none() {
let syslog_layer: Option<Filtered<_, _, Registry>> = if config.log_file.is_none() {
// TODO decide how to configure the filter for syslog
let env_filter = create_env_filter(&config.default_filter);
// Don't fail if syslog isn't available on the system, since it's a default
Expand Down
Loading