mirror of
https://github.com/rojo-rbx/rojo.git
synced 2026-10-23 05:51:39 +00:00
Update logging and enhance .gitignore for Atlas directory
- Introduced a logging system using `tracing` and related crates to improve log management and output formatting. - Updated `.gitignore` to include the `.atlas/` directory, ensuring that log files and other generated content are not tracked by Git. - Enhanced project templates to include the new `.gitignore` settings for Atlas in various project types. These changes aim to improve logging capabilities and maintain a cleaner repository by ignoring unnecessary files.
This commit is contained in:
parent
b5583c4c1b
commit
3fd50df0b2
29 changed files with 1463 additions and 79 deletions
|
|
@ -14,7 +14,12 @@ mod studio;
|
|||
mod syncback;
|
||||
mod upload;
|
||||
|
||||
use std::{borrow::Cow, env, path::Path, str::FromStr};
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
env,
|
||||
path::{Path, PathBuf},
|
||||
str::FromStr,
|
||||
};
|
||||
|
||||
use clap::Parser;
|
||||
use thiserror::Error;
|
||||
|
|
@ -110,16 +115,6 @@ impl From<ColorChoice> for termcolor::ColorChoice {
|
|||
}
|
||||
}
|
||||
|
||||
impl From<ColorChoice> for env_logger::WriteStyle {
|
||||
fn from(value: ColorChoice) -> Self {
|
||||
match value {
|
||||
ColorChoice::Auto => env_logger::WriteStyle::Auto,
|
||||
ColorChoice::Always => env_logger::WriteStyle::Always,
|
||||
ColorChoice::Never => env_logger::WriteStyle::Never,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
#[error("Invalid color choice '{attempted}'. Valid values are: auto, always, never")]
|
||||
pub struct ColorChoiceParseError {
|
||||
|
|
@ -146,10 +141,62 @@ pub enum Subcommand {
|
|||
Pull(SyncbackCommand),
|
||||
}
|
||||
|
||||
pub(super) fn resolve_path(path: &Path) -> Cow<'_, Path> {
|
||||
impl Subcommand {
|
||||
pub fn project_path(&self) -> Option<&Path> {
|
||||
match self {
|
||||
Subcommand::Clone(cmd) => cmd.path.as_deref(),
|
||||
Subcommand::Serve(cmd) => Some(&cmd.project),
|
||||
Subcommand::Build(cmd) => Some(&cmd.project),
|
||||
Subcommand::Upload(cmd) => Some(&cmd.project),
|
||||
Subcommand::Sourcemap(cmd) => Some(&cmd.project),
|
||||
Subcommand::FmtProject(cmd) => Some(&cmd.project),
|
||||
Subcommand::Studio(cmd) => Some(&cmd.project),
|
||||
Subcommand::Syncback(cmd) | Subcommand::Pull(cmd) => Some(&cmd.project),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn command_name(&self) -> &'static str {
|
||||
match self {
|
||||
Subcommand::Clone(_) => "clone",
|
||||
Subcommand::Completions(_) => "completions",
|
||||
Subcommand::Init(_) => "init",
|
||||
Subcommand::Serve(_) => "serve",
|
||||
Subcommand::Build(_) => "build",
|
||||
Subcommand::Upload(_) => "upload",
|
||||
Subcommand::Sourcemap(_) => "sourcemap",
|
||||
Subcommand::FmtProject(_) => "fmt-project",
|
||||
Subcommand::Cursor(_) => "cursor",
|
||||
Subcommand::Doc(_) => "doc",
|
||||
Subcommand::Plugin(_) => "plugin",
|
||||
Subcommand::Studio(_) => "studio",
|
||||
Subcommand::Syncback(_) => "syncback",
|
||||
Subcommand::Pull(_) => "pull",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn resolve_path(path: &Path) -> Cow<'_, Path> {
|
||||
if path.is_absolute() {
|
||||
Cow::Borrowed(path)
|
||||
} else {
|
||||
Cow::Owned(env::current_dir().unwrap().join(path))
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolves a project path (which may point to a file) to its parent directory.
|
||||
pub fn resolve_project_dir(project_path: &Path) -> PathBuf {
|
||||
let resolved = resolve_path(project_path);
|
||||
let resolved = resolved.as_ref();
|
||||
|
||||
if resolved.is_file() {
|
||||
resolved
|
||||
.parent()
|
||||
.map(|p| p.to_path_buf())
|
||||
.unwrap_or_else(|| resolved.to_path_buf())
|
||||
} else if resolved.as_os_str().is_empty() {
|
||||
env::current_dir().unwrap_or_else(|_| PathBuf::from("."))
|
||||
} else {
|
||||
resolved.to_path_buf()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ mod change_processor;
|
|||
pub mod git;
|
||||
mod glob;
|
||||
mod json;
|
||||
pub mod logging;
|
||||
mod lua_ast;
|
||||
mod message_queue;
|
||||
mod multimap;
|
||||
|
|
|
|||
521
src/logging.rs
Normal file
521
src/logging.rs
Normal file
|
|
@ -0,0 +1,521 @@
|
|||
use std::{
|
||||
io::{self, IsTerminal, Write},
|
||||
path::Path,
|
||||
};
|
||||
|
||||
use tracing_subscriber::{
|
||||
fmt::{self, time::UtcTime},
|
||||
layer::SubscriberExt,
|
||||
util::SubscriberInitExt,
|
||||
EnvFilter, Layer,
|
||||
};
|
||||
|
||||
use crate::cli::ColorChoice;
|
||||
|
||||
pub struct LogGuard {
|
||||
_file_guard: Option<tracing_appender::non_blocking::WorkerGuard>,
|
||||
}
|
||||
|
||||
pub fn init_logging(
|
||||
verbosity: u8,
|
||||
color: ColorChoice,
|
||||
project_dir: Option<&Path>,
|
||||
file_log_level: Option<tracing::level_filters::LevelFilter>,
|
||||
command_name: &str,
|
||||
) -> LogGuard {
|
||||
tracing_log::LogTracer::init().expect("Failed to set log tracer");
|
||||
|
||||
let console_filter = match verbosity {
|
||||
0 => "info",
|
||||
1 => "info,librojo=debug",
|
||||
2 => "info,librojo=trace",
|
||||
_ => "trace",
|
||||
};
|
||||
|
||||
let console_env_filter =
|
||||
EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(console_filter));
|
||||
|
||||
let use_ansi = match color {
|
||||
ColorChoice::Always => true,
|
||||
ColorChoice::Never => false,
|
||||
ColorChoice::Auto => io::stderr().is_terminal(),
|
||||
};
|
||||
|
||||
let console_layer = fmt::layer()
|
||||
.with_writer(io::stderr)
|
||||
.with_ansi(use_ansi)
|
||||
.without_time()
|
||||
.with_target(false)
|
||||
.with_thread_names(false)
|
||||
.with_level(true)
|
||||
.with_filter(console_env_filter);
|
||||
|
||||
let mut file_guard: Option<tracing_appender::non_blocking::WorkerGuard> = None;
|
||||
|
||||
let file_layer = if let (Some(dir), Some(level)) = (project_dir, file_log_level) {
|
||||
let log_dir = dir.join(".atlas").join("logs");
|
||||
|
||||
match std::fs::create_dir_all(&log_dir) {
|
||||
Ok(()) => {
|
||||
compress_old_logs(&log_dir, command_name);
|
||||
|
||||
let file_appender = tracing_appender::rolling::Builder::new()
|
||||
.rotation(tracing_appender::rolling::Rotation::DAILY)
|
||||
.filename_prefix(command_name)
|
||||
.filename_suffix("log")
|
||||
.build(&log_dir)
|
||||
.expect("Failed to create rolling file appender");
|
||||
|
||||
let (non_blocking, guard) = tracing_appender::non_blocking(file_appender);
|
||||
file_guard = Some(guard);
|
||||
|
||||
let file_filter = EnvFilter::new(level.to_string());
|
||||
|
||||
let layer = fmt::layer()
|
||||
.with_writer(non_blocking)
|
||||
.with_ansi(false)
|
||||
.with_timer(UtcTime::rfc_3339())
|
||||
.with_target(true)
|
||||
.with_thread_names(true)
|
||||
.with_level(true)
|
||||
.with_filter(file_filter);
|
||||
|
||||
Some(layer)
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!(
|
||||
"Warning: could not create log directory {}: {e}",
|
||||
log_dir.display()
|
||||
);
|
||||
None
|
||||
}
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
tracing_subscriber::registry()
|
||||
.with(console_layer)
|
||||
.with(file_layer)
|
||||
.init();
|
||||
|
||||
LogGuard {
|
||||
_file_guard: file_guard,
|
||||
}
|
||||
}
|
||||
|
||||
fn compress_old_logs(log_dir: &Path, command_name: &str) {
|
||||
let today = {
|
||||
let now = std::time::SystemTime::now();
|
||||
let since_epoch = now
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap_or_default();
|
||||
let days = since_epoch.as_secs() / 86400;
|
||||
days
|
||||
};
|
||||
|
||||
let entries = match std::fs::read_dir(log_dir) {
|
||||
Ok(entries) => entries,
|
||||
Err(_) => return,
|
||||
};
|
||||
|
||||
for entry in entries.flatten() {
|
||||
let path = entry.path();
|
||||
let file_name = match path.file_name().and_then(|n| n.to_str()) {
|
||||
Some(n) => n.to_owned(),
|
||||
None => continue,
|
||||
};
|
||||
|
||||
if file_name.ends_with(".log.gz") {
|
||||
if let Ok(meta) = entry.metadata() {
|
||||
if let Ok(modified) = meta.modified() {
|
||||
let age_days = modified
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.map(|d| today.saturating_sub(d.as_secs() / 86400))
|
||||
.unwrap_or(0);
|
||||
if age_days > 7 {
|
||||
let _ = std::fs::remove_file(&path);
|
||||
}
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if !file_name.ends_with(".log") {
|
||||
continue;
|
||||
}
|
||||
|
||||
if !file_name.starts_with(command_name) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Ok(meta) = entry.metadata() {
|
||||
if let Ok(modified) = meta.modified() {
|
||||
let file_days = modified
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.map(|d| d.as_secs() / 86400)
|
||||
.unwrap_or(today);
|
||||
if file_days >= today {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let gz_path = path.with_extension("log.gz");
|
||||
if let Ok(input) = std::fs::read(&path) {
|
||||
let gz_file = match std::fs::File::create(&gz_path) {
|
||||
Ok(f) => f,
|
||||
Err(_) => continue,
|
||||
};
|
||||
let mut encoder =
|
||||
flate2::write::GzEncoder::new(gz_file, flate2::Compression::default());
|
||||
if encoder.write_all(&input).is_ok() && encoder.finish().is_ok() {
|
||||
let _ = std::fs::remove_file(&path);
|
||||
} else {
|
||||
let _ = std::fs::remove_file(&gz_path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Parses a Roblox log message forwarded from the plugin, extracting the real
|
||||
/// log level and a cleaned message. Rojo plugin logs have `[Rojo-Trace] ` etc.
|
||||
/// prefixes that encode the level; these are stripped and replaced with `[Rojo]`.
|
||||
/// Game script output without a Rojo prefix falls back to the `MessageType` enum.
|
||||
///
|
||||
/// Returns `(log::Level, cleaned_message)`.
|
||||
pub fn parse_roblox_log(message: &str, message_type: u64) -> (log::Level, String) {
|
||||
if let Some(rest) = message.strip_prefix("[Rojo-Trace] ") {
|
||||
(log::Level::Trace, format!("[Rojo] {}", rest))
|
||||
} else if let Some(rest) = message.strip_prefix("[Rojo-Debug] ") {
|
||||
(log::Level::Debug, format!("[Rojo] {}", rest))
|
||||
} else if let Some(rest) = message.strip_prefix("[Rojo-Info] ") {
|
||||
(log::Level::Info, format!("[Rojo] {}", rest))
|
||||
} else if let Some(rest) = message.strip_prefix("[Rojo-Warn] ") {
|
||||
(log::Level::Warn, format!("[Rojo] {}", rest))
|
||||
} else {
|
||||
let lvl = match message_type {
|
||||
2 => log::Level::Warn,
|
||||
3 => log::Level::Error,
|
||||
_ => log::Level::Info,
|
||||
};
|
||||
(lvl, message.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// Lightweight pre-read of a project file to extract the `fileLogLevel` config.
|
||||
/// Accepts either a project file path or a directory (searches for default project).
|
||||
/// Returns `None` if no project found or the field is absent (caller defaults
|
||||
/// to trace). Returns `Some(None)` when `"none"` (disabled).
|
||||
pub fn quick_read_file_log_level(
|
||||
path: &Path,
|
||||
) -> Option<Option<tracing::level_filters::LevelFilter>> {
|
||||
use tracing::level_filters::LevelFilter;
|
||||
|
||||
let project_file = if path.is_file() {
|
||||
path.to_path_buf()
|
||||
} else {
|
||||
let json5_path = path.join("default.project.json5");
|
||||
if json5_path.is_file() {
|
||||
json5_path
|
||||
} else {
|
||||
let json_path = path.join("default.project.json");
|
||||
if json_path.is_file() {
|
||||
json_path
|
||||
} else {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let contents = std::fs::read_to_string(&project_file).ok()?;
|
||||
let val: serde_json::Value = json5::from_str(&contents).ok()?;
|
||||
let level_str = val.get("fileLogLevel")?.as_str()?;
|
||||
|
||||
Some(match level_str.to_lowercase().as_str() {
|
||||
"none" | "off" => None,
|
||||
"error" => Some(LevelFilter::ERROR),
|
||||
"warn" => Some(LevelFilter::WARN),
|
||||
"info" => Some(LevelFilter::INFO),
|
||||
"debug" => Some(LevelFilter::DEBUG),
|
||||
"trace" => Some(LevelFilter::TRACE),
|
||||
_ => Some(LevelFilter::TRACE),
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::io::Read;
|
||||
use tracing::level_filters::LevelFilter;
|
||||
|
||||
#[test]
|
||||
fn parse_roblox_log_rojo_trace() {
|
||||
let (level, msg) = parse_roblox_log("[Rojo-Trace] snapshot started", 0);
|
||||
assert_eq!(level, log::Level::Trace);
|
||||
assert_eq!(msg, "[Rojo] snapshot started");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_roblox_log_rojo_debug() {
|
||||
let (level, msg) = parse_roblox_log("[Rojo-Debug] loading project", 0);
|
||||
assert_eq!(level, log::Level::Debug);
|
||||
assert_eq!(msg, "[Rojo] loading project");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_roblox_log_rojo_info() {
|
||||
let (level, msg) = parse_roblox_log("[Rojo-Info] connected to server", 0);
|
||||
assert_eq!(level, log::Level::Info);
|
||||
assert_eq!(msg, "[Rojo] connected to server");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_roblox_log_rojo_warn() {
|
||||
let (level, msg) = parse_roblox_log("[Rojo-Warn] something fishy", 0);
|
||||
assert_eq!(level, log::Level::Warn);
|
||||
assert_eq!(msg, "[Rojo] something fishy");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_roblox_log_game_print() {
|
||||
let (level, msg) = parse_roblox_log("hello from game", 0);
|
||||
assert_eq!(level, log::Level::Info);
|
||||
assert_eq!(msg, "hello from game");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_roblox_log_game_warn() {
|
||||
let (level, msg) = parse_roblox_log("something wrong", 2);
|
||||
assert_eq!(level, log::Level::Warn);
|
||||
assert_eq!(msg, "something wrong");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_roblox_log_game_error() {
|
||||
let (level, msg) = parse_roblox_log("crash!", 3);
|
||||
assert_eq!(level, log::Level::Error);
|
||||
assert_eq!(msg, "crash!");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_roblox_log_message_info_type() {
|
||||
let (level, msg) = parse_roblox_log("info message", 1);
|
||||
assert_eq!(level, log::Level::Info);
|
||||
assert_eq!(msg, "info message");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_roblox_log_unknown_message_type() {
|
||||
let (level, msg) = parse_roblox_log("weird type", 99);
|
||||
assert_eq!(level, log::Level::Info);
|
||||
assert_eq!(msg, "weird type");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_roblox_log_partial_prefix_not_stripped() {
|
||||
let (level, msg) = parse_roblox_log("[Rojo-Trace]no space", 0);
|
||||
assert_eq!(level, log::Level::Info);
|
||||
assert_eq!(msg, "[Rojo-Trace]no space");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_roblox_log_empty_message_after_prefix() {
|
||||
let (level, msg) = parse_roblox_log("[Rojo-Info] ", 0);
|
||||
assert_eq!(level, log::Level::Info);
|
||||
assert_eq!(msg, "[Rojo] ");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn quick_read_file_log_level_trace() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
std::fs::write(
|
||||
dir.path().join("default.project.json5"),
|
||||
r#"{ "name": "Test", "tree": {}, "fileLogLevel": "trace" }"#,
|
||||
)
|
||||
.unwrap();
|
||||
let result = quick_read_file_log_level(dir.path());
|
||||
assert_eq!(result, Some(Some(LevelFilter::TRACE)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn quick_read_file_log_level_none() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
std::fs::write(
|
||||
dir.path().join("default.project.json5"),
|
||||
r#"{ "name": "Test", "tree": {}, "fileLogLevel": "none" }"#,
|
||||
)
|
||||
.unwrap();
|
||||
let result = quick_read_file_log_level(dir.path());
|
||||
assert_eq!(result, Some(None));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn quick_read_file_log_level_off() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
std::fs::write(
|
||||
dir.path().join("default.project.json5"),
|
||||
r#"{ "name": "Test", "tree": {}, "fileLogLevel": "off" }"#,
|
||||
)
|
||||
.unwrap();
|
||||
let result = quick_read_file_log_level(dir.path());
|
||||
assert_eq!(result, Some(None));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn quick_read_file_log_level_warn() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
std::fs::write(
|
||||
dir.path().join("default.project.json5"),
|
||||
r#"{ "name": "Test", "tree": {}, "fileLogLevel": "warn" }"#,
|
||||
)
|
||||
.unwrap();
|
||||
let result = quick_read_file_log_level(dir.path());
|
||||
assert_eq!(result, Some(Some(LevelFilter::WARN)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn quick_read_file_log_level_case_insensitive() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
std::fs::write(
|
||||
dir.path().join("default.project.json5"),
|
||||
r#"{ "name": "Test", "tree": {}, "fileLogLevel": "DEBUG" }"#,
|
||||
)
|
||||
.unwrap();
|
||||
let result = quick_read_file_log_level(dir.path());
|
||||
assert_eq!(result, Some(Some(LevelFilter::DEBUG)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn quick_read_file_log_level_absent() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
std::fs::write(
|
||||
dir.path().join("default.project.json5"),
|
||||
r#"{ "name": "Test", "tree": {} }"#,
|
||||
)
|
||||
.unwrap();
|
||||
let result = quick_read_file_log_level(dir.path());
|
||||
assert_eq!(result, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn quick_read_file_log_level_no_project() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let result = quick_read_file_log_level(dir.path());
|
||||
assert_eq!(result, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn quick_read_file_log_level_legacy_json() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
std::fs::write(
|
||||
dir.path().join("default.project.json"),
|
||||
r#"{ "name": "Test", "tree": {}, "fileLogLevel": "error" }"#,
|
||||
)
|
||||
.unwrap();
|
||||
let result = quick_read_file_log_level(dir.path());
|
||||
assert_eq!(result, Some(Some(LevelFilter::ERROR)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn quick_read_file_log_level_json5_preferred_over_json() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
std::fs::write(
|
||||
dir.path().join("default.project.json5"),
|
||||
r#"{ "name": "Test", "tree": {}, "fileLogLevel": "info" }"#,
|
||||
)
|
||||
.unwrap();
|
||||
std::fs::write(
|
||||
dir.path().join("default.project.json"),
|
||||
r#"{ "name": "Test", "tree": {}, "fileLogLevel": "error" }"#,
|
||||
)
|
||||
.unwrap();
|
||||
let result = quick_read_file_log_level(dir.path());
|
||||
assert_eq!(result, Some(Some(LevelFilter::INFO)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn quick_read_file_log_level_direct_file_path() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let file_path = dir.path().join("custom.project.json5");
|
||||
std::fs::write(
|
||||
&file_path,
|
||||
r#"{ "name": "Test", "tree": {}, "fileLogLevel": "debug" }"#,
|
||||
)
|
||||
.unwrap();
|
||||
let result = quick_read_file_log_level(&file_path);
|
||||
assert_eq!(result, Some(Some(LevelFilter::DEBUG)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn quick_read_file_log_level_unknown_defaults_to_trace() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
std::fs::write(
|
||||
dir.path().join("default.project.json5"),
|
||||
r#"{ "name": "Test", "tree": {}, "fileLogLevel": "banana" }"#,
|
||||
)
|
||||
.unwrap();
|
||||
let result = quick_read_file_log_level(dir.path());
|
||||
assert_eq!(result, Some(Some(LevelFilter::TRACE)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compress_old_logs_compresses_old_files() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let log_dir = dir.path();
|
||||
|
||||
let old_file = log_dir.join("atlas-serve.2020-01-01.log");
|
||||
std::fs::write(&old_file, "old log content").unwrap();
|
||||
|
||||
let mtime =
|
||||
std::time::SystemTime::UNIX_EPOCH + std::time::Duration::from_secs(86400 * 18262);
|
||||
filetime::set_file_mtime(&old_file, filetime::FileTime::from_system_time(mtime))
|
||||
.unwrap_or_default();
|
||||
|
||||
compress_old_logs(log_dir, "atlas-serve");
|
||||
|
||||
assert!(!old_file.exists(), "original .log file should be deleted");
|
||||
let gz_file = log_dir.join("atlas-serve.2020-01-01.log.gz");
|
||||
assert!(gz_file.exists(), ".log.gz file should be created");
|
||||
|
||||
let gz_data = std::fs::read(&gz_file).unwrap();
|
||||
let mut decoder = flate2::read::GzDecoder::new(&gz_data[..]);
|
||||
let mut decompressed = String::new();
|
||||
decoder.read_to_string(&mut decompressed).unwrap();
|
||||
assert_eq!(decompressed, "old log content");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compress_old_logs_skips_other_commands() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let log_dir = dir.path();
|
||||
|
||||
let other_file = log_dir.join("atlas-build.2020-01-01.log");
|
||||
std::fs::write(&other_file, "build log").unwrap();
|
||||
|
||||
let mtime =
|
||||
std::time::SystemTime::UNIX_EPOCH + std::time::Duration::from_secs(86400 * 18262);
|
||||
filetime::set_file_mtime(&other_file, filetime::FileTime::from_system_time(mtime))
|
||||
.unwrap_or_default();
|
||||
|
||||
compress_old_logs(log_dir, "atlas-serve");
|
||||
|
||||
assert!(
|
||||
other_file.exists(),
|
||||
"other command's log should NOT be compressed"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compress_old_logs_skips_today() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let log_dir = dir.path();
|
||||
|
||||
let today_file = log_dir.join("atlas-serve.today.log");
|
||||
std::fs::write(&today_file, "today's log").unwrap();
|
||||
|
||||
compress_old_logs(log_dir, "atlas-serve");
|
||||
|
||||
assert!(today_file.exists(), "today's log should NOT be compressed");
|
||||
}
|
||||
}
|
||||
36
src/main.rs
36
src/main.rs
|
|
@ -3,15 +3,14 @@ use std::{env, panic, process};
|
|||
use backtrace::Backtrace;
|
||||
use clap::Parser;
|
||||
|
||||
use librojo::cli::Options;
|
||||
use librojo::cli::{resolve_project_dir, Options};
|
||||
use librojo::logging;
|
||||
|
||||
fn main() {
|
||||
#[cfg(feature = "profile-with-tracy")]
|
||||
profiling::tracy_client::Client::start();
|
||||
|
||||
panic::set_hook(Box::new(|panic_info| {
|
||||
// PanicInfo's payload is usually a &'static str or String.
|
||||
// See: https://doc.rust-lang.org/beta/std/panic/struct.PanicInfo.html#method.payload
|
||||
let message = match panic_info.payload().downcast_ref::<&str>() {
|
||||
Some(&message) => message.to_string(),
|
||||
None => match panic_info.payload().downcast_ref::<String>() {
|
||||
|
|
@ -37,9 +36,6 @@ fn main() {
|
|||
log::error!("in file {} on line {}", location.file(), location.line());
|
||||
}
|
||||
|
||||
// When using the backtrace crate, we need to check the RUST_BACKTRACE
|
||||
// environment variable ourselves. Once we switch to the (currently
|
||||
// unstable) std::backtrace module, we won't need to do this anymore.
|
||||
let should_backtrace = env::var("RUST_BACKTRACE")
|
||||
.map(|var| var == "1")
|
||||
.unwrap_or(false);
|
||||
|
|
@ -57,22 +53,22 @@ fn main() {
|
|||
|
||||
let options = Options::parse();
|
||||
|
||||
let log_filter = match options.global.verbosity {
|
||||
0 => "info",
|
||||
1 => "info,librojo=debug",
|
||||
2 => "info,librojo=trace",
|
||||
_ => "trace",
|
||||
};
|
||||
let project_dir = options.subcommand.project_path().map(resolve_project_dir);
|
||||
|
||||
let log_env = env_logger::Env::default().default_filter_or(log_filter);
|
||||
let file_log_level = project_dir
|
||||
.as_deref()
|
||||
.and_then(logging::quick_read_file_log_level)
|
||||
.unwrap_or(Some(tracing::level_filters::LevelFilter::TRACE));
|
||||
|
||||
env_logger::Builder::from_env(log_env)
|
||||
.format_module_path(false)
|
||||
.format_timestamp(None)
|
||||
// Indent following lines equal to the log level label, like `[ERROR] `
|
||||
.format_indent(Some(8))
|
||||
.write_style(options.global.color.into())
|
||||
.init();
|
||||
let command_name = format!("atlas-{}", options.subcommand.command_name());
|
||||
|
||||
let _log_guard = logging::init_logging(
|
||||
options.global.verbosity,
|
||||
options.global.color,
|
||||
project_dir.as_deref(),
|
||||
file_log_level,
|
||||
&command_name,
|
||||
);
|
||||
|
||||
if let Err(err) = options.run() {
|
||||
log::error!("{:?}", err);
|
||||
|
|
|
|||
|
|
@ -142,6 +142,12 @@ pub struct Project {
|
|||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub ignore_hidden_services: Option<bool>,
|
||||
|
||||
/// Controls the log level for file-based logging to `.atlas/logs/`.
|
||||
/// Values: "trace" (default), "debug", "info", "warn", "error", "none".
|
||||
/// When "none", file logging is disabled entirely.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub file_log_level: Option<String>,
|
||||
|
||||
/// The path to the file that this project came from. Relative paths in the
|
||||
/// project should be considered relative to the parent of this field, also
|
||||
/// given by `Project::folder_location`.
|
||||
|
|
|
|||
|
|
@ -383,7 +383,7 @@ mod test {
|
|||
|
||||
#[test]
|
||||
fn add_from_empty() {
|
||||
let _ = env_logger::try_init();
|
||||
let _ = tracing_subscriber::fmt::try_init();
|
||||
|
||||
let mut tree = RojoTree::new(InstanceSnapshot::new());
|
||||
|
||||
|
|
@ -421,7 +421,7 @@ mod test {
|
|||
|
||||
#[test]
|
||||
fn update_existing() {
|
||||
let _ = env_logger::try_init();
|
||||
let _ = tracing_subscriber::fmt::try_init();
|
||||
|
||||
let mut tree = RojoTree::new(
|
||||
InstanceSnapshot::new()
|
||||
|
|
|
|||
|
|
@ -985,7 +985,7 @@ mod test {
|
|||
#[ignore = "Functionality moved to root snapshot middleware"]
|
||||
#[test]
|
||||
fn project_from_folder() {
|
||||
let _ = env_logger::try_init();
|
||||
let _ = tracing_subscriber::fmt::try_init();
|
||||
|
||||
let mut imfs = InMemoryFs::new();
|
||||
imfs.load_snapshot(
|
||||
|
|
@ -1022,7 +1022,7 @@ mod test {
|
|||
|
||||
#[test]
|
||||
fn project_from_direct_file() {
|
||||
let _ = env_logger::try_init();
|
||||
let _ = tracing_subscriber::fmt::try_init();
|
||||
|
||||
let mut imfs = InMemoryFs::new();
|
||||
imfs.load_snapshot(
|
||||
|
|
@ -1059,7 +1059,7 @@ mod test {
|
|||
|
||||
#[test]
|
||||
fn project_with_resolved_properties() {
|
||||
let _ = env_logger::try_init();
|
||||
let _ = tracing_subscriber::fmt::try_init();
|
||||
|
||||
let mut imfs = InMemoryFs::new();
|
||||
imfs.load_snapshot(
|
||||
|
|
@ -1098,7 +1098,7 @@ mod test {
|
|||
|
||||
#[test]
|
||||
fn project_with_unresolved_properties() {
|
||||
let _ = env_logger::try_init();
|
||||
let _ = tracing_subscriber::fmt::try_init();
|
||||
|
||||
let mut imfs = InMemoryFs::new();
|
||||
imfs.load_snapshot(
|
||||
|
|
@ -1135,7 +1135,7 @@ mod test {
|
|||
|
||||
#[test]
|
||||
fn project_with_children() {
|
||||
let _ = env_logger::try_init();
|
||||
let _ = tracing_subscriber::fmt::try_init();
|
||||
|
||||
let mut imfs = InMemoryFs::new();
|
||||
imfs.load_snapshot(
|
||||
|
|
@ -1173,7 +1173,7 @@ mod test {
|
|||
|
||||
#[test]
|
||||
fn project_with_path_to_txt() {
|
||||
let _ = env_logger::try_init();
|
||||
let _ = tracing_subscriber::fmt::try_init();
|
||||
|
||||
let mut imfs = InMemoryFs::new();
|
||||
imfs.load_snapshot(
|
||||
|
|
@ -1213,7 +1213,7 @@ mod test {
|
|||
|
||||
#[test]
|
||||
fn project_with_path_to_project() {
|
||||
let _ = env_logger::try_init();
|
||||
let _ = tracing_subscriber::fmt::try_init();
|
||||
|
||||
let mut imfs = InMemoryFs::new();
|
||||
imfs.load_snapshot(
|
||||
|
|
@ -1265,7 +1265,7 @@ mod test {
|
|||
|
||||
#[test]
|
||||
fn project_with_path_to_project_with_children() {
|
||||
let _ = env_logger::try_init();
|
||||
let _ = tracing_subscriber::fmt::try_init();
|
||||
|
||||
let mut imfs = InMemoryFs::new();
|
||||
imfs.load_snapshot(
|
||||
|
|
@ -1324,7 +1324,7 @@ mod test {
|
|||
/// precedence.
|
||||
#[test]
|
||||
fn project_path_property_overrides() {
|
||||
let _ = env_logger::try_init();
|
||||
let _ = tracing_subscriber::fmt::try_init();
|
||||
|
||||
let mut imfs = InMemoryFs::new();
|
||||
imfs.load_snapshot(
|
||||
|
|
@ -1382,7 +1382,7 @@ mod test {
|
|||
|
||||
#[test]
|
||||
fn no_name_project() {
|
||||
let _ = env_logger::try_init();
|
||||
let _ = tracing_subscriber::fmt::try_init();
|
||||
|
||||
let mut imfs = InMemoryFs::new();
|
||||
imfs.load_snapshot(
|
||||
|
|
|
|||
|
|
@ -3878,7 +3878,15 @@ async fn handle_mcp_stream_connection(
|
|||
match msg {
|
||||
Some(Ok(Message::Text(text))) => {
|
||||
if let Ok(mut result_value) = serde_json::from_str::<serde_json::Value>(&text) {
|
||||
// Enrich based on the command type that produced this result.
|
||||
if result_value.get("type").and_then(|t| t.as_str()) == Some("roblox_log") {
|
||||
if let Some(message) = result_value.get("message").and_then(|m| m.as_str()) {
|
||||
let message_type = result_value.get("level").and_then(|l| l.as_u64()).unwrap_or(0);
|
||||
let (level, formatted) = crate::logging::parse_roblox_log(message, message_type);
|
||||
log::log!(target: "roblox", level, "{}", formatted);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(ctx) = pending_get_script_context.take() {
|
||||
enrich_get_script_result(&mut result_value, &ctx);
|
||||
} else if let Some(changes) = result_value.get_mut("changes") {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue