feat: improve legacy metadata and admin compatibility (#2202)

This commit is contained in:
weisd
2026-03-18 21:05:09 +08:00
committed by GitHub
parent 84077adf17
commit b9b7d86ae4
133 changed files with 11707 additions and 1945 deletions
+39 -1
View File
@@ -96,7 +96,14 @@ pub(super) fn init_local_logging(
let log_dir_str = config.log_directory.as_deref().filter(|s| !s.is_empty());
if let Some(log_directory) = log_dir_str {
init_file_logging_internal(config, log_directory, logger_level, is_production)
match init_file_logging_internal(config, log_directory, logger_level, is_production) {
Ok(guard) => Ok(guard),
Err(error) if should_fallback_to_stdout(&error) => {
emit_file_logging_fallback_warning(log_directory, &error);
Ok(init_stdout_only(config, logger_level, is_production))
}
Err(error) => Err(error),
}
} else {
Ok(init_stdout_only(config, logger_level, is_production))
}
@@ -331,6 +338,24 @@ pub fn ensure_dir_permissions(log_directory: &str) -> Result<(), TelemetryError>
}
}
pub(super) fn should_fallback_to_stdout(error: &TelemetryError) -> bool {
match error {
TelemetryError::SetPermissions(_) => true,
TelemetryError::Io(message) => {
let message = message.to_ascii_lowercase();
message.contains("permission denied") || message.contains("os error 13")
}
_ => false,
}
}
pub(super) fn emit_file_logging_fallback_warning(log_directory: &str, error: &TelemetryError) {
eprintln!(
"[WARN] Failed to initialize file observability logging at '{}': {}. Falling back to stdout logging.",
log_directory, error
);
}
// ─── Cleanup task ─────────────────────────────────────────────────────────────
/// Spawn a background task that periodically cleans up old log files.
@@ -496,4 +521,17 @@ mod tests {
assert!(result.is_err(), "invalid filename must return Err, not panic");
});
}
#[test]
fn test_permission_denied_errors_fall_back_to_stdout() {
assert!(should_fallback_to_stdout(&TelemetryError::Io(
"Permission denied (os error 13)".to_string()
)));
assert!(should_fallback_to_stdout(&TelemetryError::SetPermissions(
"dir='/logs', want=0o755, have=0o777, err=Permission denied (os error 13)".to_string()
)));
assert!(!should_fallback_to_stdout(&TelemetryError::Io(
"No such file or directory (os error 2)".to_string()
)));
}
}
+48 -43
View File
@@ -175,6 +175,7 @@ pub(super) fn init_observability_http(
let mut cleanup_handle = None;
let mut tracing_guard = None; // Guard for file writer
let mut stdout_guard = None; // Guard for stdout writer (File mode)
let mut force_stdout_logging = false;
// ── Case 1: OTLP Logging
if !log_ep.is_empty() {
@@ -201,40 +202,34 @@ pub(super) fn init_observability_http(
{
let log_filename = config.log_filename.as_deref().unwrap_or(&service_name);
let keep_files = config.log_keep_files.unwrap_or(DEFAULT_LOG_KEEP_FILES);
let file_logging_result = (|| -> Result<_, TelemetryError> {
fs::create_dir_all(log_directory).map_err(|e| TelemetryError::Io(e.to_string()))?;
// 1. Ensure dir exists
if let Err(e) = fs::create_dir_all(log_directory) {
return Err(TelemetryError::Io(e.to_string()));
}
// 2. Permissions
#[cfg(unix)]
crate::telemetry::local::ensure_dir_permissions(log_directory)?;
#[cfg(unix)]
crate::telemetry::local::ensure_dir_permissions(log_directory)?;
// 3. Rotation
let rotation_str = config
.log_rotation_time
.as_deref()
.unwrap_or(DEFAULT_LOG_ROTATION_TIME)
.to_lowercase();
let match_mode = FileMatchMode::from_config_str(config.log_match_mode.as_deref().unwrap_or(DEFAULT_OBS_LOG_MATCH_MODE));
let rotation = match rotation_str.as_str() {
"minutely" => Rotation::Minutely,
"hourly" => Rotation::Hourly,
"daily" => Rotation::Daily,
_ => Rotation::Daily,
};
let max_single_file_size = config
.log_max_single_file_size_bytes
.unwrap_or(DEFAULT_OBS_LOG_MAX_SINGLE_FILE_SIZE_BYTES);
let rotation_str = config
.log_rotation_time
.as_deref()
.unwrap_or(DEFAULT_LOG_ROTATION_TIME)
.to_lowercase();
let match_mode =
FileMatchMode::from_config_str(config.log_match_mode.as_deref().unwrap_or(DEFAULT_OBS_LOG_MATCH_MODE));
let rotation = match rotation_str.as_str() {
"minutely" => Rotation::Minutely,
"hourly" => Rotation::Hourly,
"daily" => Rotation::Daily,
_ => Rotation::Daily,
};
let max_single_file_size = config
.log_max_single_file_size_bytes
.unwrap_or(DEFAULT_OBS_LOG_MAX_SINGLE_FILE_SIZE_BYTES);
let file_appender =
RollingAppender::new(log_directory, log_filename.to_string(), rotation, max_single_file_size, match_mode)?;
let file_appender =
RollingAppender::new(log_directory, log_filename.to_string(), rotation, max_single_file_size, match_mode)?;
let (non_blocking, guard) = tracing_appender::non_blocking(file_appender);
tracing_guard = Some(guard);
file_layer_opt = Some(
tracing_subscriber::fmt::layer()
let (non_blocking, guard) = tracing_appender::non_blocking(file_appender);
let file_layer = tracing_subscriber::fmt::layer()
.with_timer(LocalTime::rfc_3339())
.with_target(true)
.with_ansi(false)
@@ -247,17 +242,28 @@ pub(super) fn init_observability_http(
.with_current_span(true)
.with_span_list(true)
.with_span_events(span_events.clone())
.with_filter(build_env_filter(logger_level, None)),
);
.with_filter(build_env_filter(logger_level, None));
let cleanup_handle = spawn_cleanup_task(config, log_directory, log_filename, keep_files);
Ok((file_layer, guard, cleanup_handle, rotation_str))
})();
// The cleanup task keeps rotated files bounded while the OTLP trace and
// metric exporters continue to operate independently.
cleanup_handle = Some(spawn_cleanup_task(config, log_directory, log_filename, keep_files));
match file_logging_result {
Ok((file_layer, guard, new_cleanup_handle, rotation_str)) => {
tracing_guard = Some(guard);
file_layer_opt = Some(file_layer);
cleanup_handle = Some(new_cleanup_handle);
info!(
"Init file logging at '{}', rotation: {}, keep {} files",
log_directory, rotation_str, keep_files
);
info!(
"Init file logging at '{}', rotation: {}, keep {} files",
log_directory, rotation_str, keep_files
);
}
Err(error) if crate::telemetry::local::should_fallback_to_stdout(&error) => {
crate::telemetry::local::emit_file_logging_fallback_warning(log_directory, &error);
force_stdout_logging = true;
}
Err(error) => return Err(error),
}
}
// ── Tracing subscriber registry ───────────────────────────────────────────
@@ -266,10 +272,9 @@ pub(super) fn init_observability_http(
.map(|p| OpenTelemetryLayer::new(p.tracer(service_name.to_string())));
let metrics_layer = meter_provider.as_ref().map(|p| MetricsLayer::new(p.clone()));
// Optional stdout mirror (matching `init_file_logging_internal` logic).
// This is separate from OTLP stdout exporting; it only affects local human
// readable output for the tracing subscriber.
if config.log_stdout_enabled.unwrap_or(DEFAULT_OBS_LOG_STDOUT_ENABLED) || !is_production {
// Optional stdout mirror (matching init_file_logging_internal logic)
// This is separate from OTLP stdout logic. If file logging is enabled, we honor its stdout rules.
if force_stdout_logging || config.log_stdout_enabled.unwrap_or(DEFAULT_OBS_LOG_STDOUT_ENABLED) || !is_production {
let (stdout_nb, stdout_g) = tracing_appender::non_blocking(std::io::stdout());
stdout_guard = Some(stdout_g);
stdout_layer_opt = Some(