diff --git a/Cargo.lock b/Cargo.lock index 30cce12df..20b015517 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1690,6 +1690,7 @@ dependencies = [ "serde_yaml", "sha2", "shlex", + "temp-env", "tempfile", "thiserror 2.0.18", "tokio", @@ -1755,6 +1756,7 @@ dependencies = [ "thiserror 2.0.18", "toml 0.8.23", "tracing", + "tracing-subscriber", "ulid", ] diff --git a/Dockerfile b/Dockerfile index 0ced73368..5fa65d8aa 100644 --- a/Dockerfile +++ b/Dockerfile @@ -31,7 +31,8 @@ COPY --chmod=0755 tmp/docker-context/${TARGETARCH}/fabro /usr/local/bin/fabro COPY --chmod=0755 docker/entrypoint.sh /usr/local/bin/fabro-entrypoint ENV FABRO_HOME=/storage/.home \ - FABRO_STORAGE_DIR=/storage + FABRO_STORAGE_DIR=/storage \ + FABRO_LOG_DESTINATION=stdout VOLUME ["/storage"] EXPOSE 32276 diff --git a/docs/api-reference/fabro-api.yaml b/docs/api-reference/fabro-api.yaml index ffd692e62..6821fd16b 100644 --- a/docs/api-reference/fabro-api.yaml +++ b/docs/api-reference/fabro-api.yaml @@ -5978,10 +5978,16 @@ components: ServerLoggingSettings: type: object - required: [level] + required: [level, destination] properties: level: type: ["string", "null"] + destination: + $ref: "#/components/schemas/LogDestination" + + LogDestination: + type: string + enum: [file, stdout] ServerIntegrationsSettings: type: object diff --git a/lib/crates/fabro-api/build.rs b/lib/crates/fabro-api/build.rs index 1a3cfca87..5a2c4c42a 100644 --- a/lib/crates/fabro-api/build.rs +++ b/lib/crates/fabro-api/build.rs @@ -263,6 +263,11 @@ fn main() { "fabro_types::settings::server::ServerLoggingSettings", &[], ), + ( + "LogDestination", + "fabro_types::settings::server::LogDestination", + &[], + ), ( "ServerIntegrationsSettings", "fabro_types::settings::server::ServerIntegrationsSettings", diff --git a/lib/crates/fabro-api/src/lib.rs b/lib/crates/fabro-api/src/lib.rs index 7655ace4d..6f03faf82 100644 --- a/lib/crates/fabro-api/src/lib.rs +++ b/lib/crates/fabro-api/src/lib.rs @@ -16,12 +16,12 @@ mod generated { pub mod types { pub use fabro_types::settings::server::{ DiscordIntegrationSettings, GithubIntegrationSettings, GithubIntegrationStrategy, - IntegrationWebhooksSettings, IpAllowEntry, ObjectStoreSettings, ServerApiSettings, - ServerArtifactsSettings, ServerAuthGithubSettings, ServerAuthMethod, ServerAuthSettings, - ServerIntegrationsSettings, ServerIpAllowlistOverrideSettings, ServerIpAllowlistSettings, - ServerListenSettings, ServerLoggingSettings, ServerSchedulerSettings, - ServerSlateDbSettings, ServerStorageSettings, ServerWebSettings, SlackIntegrationSettings, - TeamsIntegrationSettings, WebhookStrategy, + IntegrationWebhooksSettings, IpAllowEntry, LogDestination, ObjectStoreSettings, + ServerApiSettings, ServerArtifactsSettings, ServerAuthGithubSettings, ServerAuthMethod, + ServerAuthSettings, ServerIntegrationsSettings, ServerIpAllowlistOverrideSettings, + ServerIpAllowlistSettings, ServerListenSettings, ServerLoggingSettings, + ServerSchedulerSettings, ServerSlateDbSettings, ServerStorageSettings, ServerWebSettings, + SlackIntegrationSettings, TeamsIntegrationSettings, WebhookStrategy, }; pub use fabro_types::settings::{FeaturesNamespace, ServerNamespace}; pub use fabro_types::status::{ diff --git a/lib/crates/fabro-api/tests/server_settings_round_trip.rs b/lib/crates/fabro-api/tests/server_settings_round_trip.rs index 2181c8884..489046d46 100644 --- a/lib/crates/fabro-api/tests/server_settings_round_trip.rs +++ b/lib/crates/fabro-api/tests/server_settings_round_trip.rs @@ -1,12 +1,13 @@ use std::any::{TypeId, type_name}; use fabro_api::types::{ - FeaturesNamespace as ApiFeaturesNamespace, ObjectStoreSettings as ApiObjectStoreSettings, - ServerNamespace as ApiServerNamespace, ServerSettings as ApiServerSettings, + FeaturesNamespace as ApiFeaturesNamespace, LogDestination as ApiLogDestination, + ObjectStoreSettings as ApiObjectStoreSettings, ServerNamespace as ApiServerNamespace, + ServerSettings as ApiServerSettings, }; use fabro_config::ServerSettingsBuilder; use fabro_types::ServerSettings; -use fabro_types::settings::server::ObjectStoreSettings; +use fabro_types::settings::server::{LogDestination, ObjectStoreSettings}; use fabro_types::settings::{FeaturesNamespace, ServerNamespace}; #[test] @@ -15,6 +16,7 @@ fn server_settings_family_reuses_domain_types() { assert_same_type::(); assert_same_type::(); assert_same_type::(); + assert_same_type::(); } #[test] @@ -43,6 +45,9 @@ allowed_usernames = ["alice"] [server.storage] root = "/srv/fabro" +[server.logging] +destination = "stdout" + [server.integrations.github] enabled = true strategy = "app" @@ -60,6 +65,7 @@ session_sandboxes = true assert_eq!(json["server"]["listen"]["type"], "tcp"); assert_eq!(json["server"]["listen"]["address"], "127.0.0.1:32276"); assert_eq!(json["server"]["storage"]["root"], "/srv/fabro"); + assert_eq!(json["server"]["logging"]["destination"], "stdout"); assert_eq!(json["features"]["session_sandboxes"], true); let round_trip: ApiServerSettings = diff --git a/lib/crates/fabro-cli/Cargo.toml b/lib/crates/fabro-cli/Cargo.toml index 8faebf72e..d73982b86 100644 --- a/lib/crates/fabro-cli/Cargo.toml +++ b/lib/crates/fabro-cli/Cargo.toml @@ -114,6 +114,7 @@ paste = "1" predicates = "3" serde_json.workspace = true tempfile = "3" +temp-env = "0.3" httpmock = "0.8" fabro-test = { workspace = true } fabro-macros = { path = "../fabro-macros" } diff --git a/lib/crates/fabro-cli/src/commands/server/start.rs b/lib/crates/fabro-cli/src/commands/server/start.rs index 85cf31c2e..eb1ecfa3b 100644 --- a/lib/crates/fabro-cli/src/commands/server/start.rs +++ b/lib/crates/fabro-cli/src/commands/server/start.rs @@ -15,7 +15,7 @@ use fabro_server::jwt_auth::auth_method_name; use fabro_server::serve::{DEFAULT_TCP_PORT, ServeArgs, resolve_runtime_server_settings_for_start}; use fabro_server::{process_env_snapshot, validate_startup}; use fabro_static::EnvVars; -use fabro_types::settings::ServerAuthMethod; +use fabro_types::settings::{LogDestination, ServerAuthMethod}; use fabro_util::printer::Printer; use fabro_util::terminal::Styles; use tokio::net::{TcpStream, UnixStream}; @@ -24,6 +24,7 @@ use tokio::task::spawn_blocking; use tokio::time; use crate::local_server; +use crate::logging::{self, ServerLogDestination}; pub(crate) struct ForegroundServerLogBootstrap { #[expect(dead_code, reason = "held for its Drop to release the server lock")] @@ -67,6 +68,7 @@ pub(crate) async fn execute( pub(crate) async fn prepare_foreground_server_log( runtime_directory: &RuntimeDirectory, + destination: &ServerLogDestination, ) -> Result { let lock_file = acquire_lock(runtime_directory).await?; if let Some(existing) = ServerDaemon::load_running(runtime_directory)? { @@ -77,13 +79,14 @@ pub(crate) async fn prepare_foreground_server_log( ); } - let log_path = runtime_directory.log_path(); - if let Some(parent) = log_path.parent() { - std::fs::create_dir_all(parent) - .with_context(|| format!("creating log directory {}", parent.display()))?; + if let ServerLogDestination::File(log_path) = destination { + if let Some(parent) = log_path.parent() { + std::fs::create_dir_all(parent) + .with_context(|| format!("creating log directory {}", parent.display()))?; + } + std::fs::File::create(log_path) + .with_context(|| format!("creating server log file {}", log_path.display()))?; } - std::fs::File::create(&log_path) - .with_context(|| format!("creating server log file {}", log_path.display()))?; Ok(ForegroundServerLogBootstrap { lock_file }) } @@ -265,6 +268,12 @@ async fn execute_daemon( } let resolved_settings = resolve_runtime_server_settings_for_start(serve_args, storage_dir)?; + let destination = logging::resolve_log_destination(resolved_settings.logging.destination)?; + if matches!(destination, LogDestination::Stdout) { + bail!( + "[server.logging].destination = \"stdout\" is incompatible with daemon mode; use `fabro server start --foreground`" + ); + } validate_startup( runtime_directory.env_path().as_path(), process_env_snapshot(), @@ -317,7 +326,7 @@ async fn execute_daemon( cmd.arg("--storage-dir").arg(storage_dir); - cmd.env_remove("FABRO_JSON"); + cmd.env_remove(EnvVars::FABRO_JSON); cmd.stdout(stdout_log) .stderr(log_file) .stdin(std::process::Stdio::null()); @@ -486,9 +495,55 @@ fn read_log_tail(log_path: &Path, lines: usize) -> String { #[cfg(test)] mod tests { + use fabro_config::bind::BindRequest; + use fabro_server::serve::ServeArgs; + use fabro_static::EnvVars; use fabro_util::Home; + use fabro_util::printer::Printer; + use temp_env::with_var; + use tokio::runtime::Runtime; - use super::ensure_storage_server_autostart_allowed; + use super::{ + ensure_storage_server_autostart_allowed, execute_daemon, prepare_foreground_server_log, + }; + use crate::logging::ServerLogDestination; + + fn runtime() -> Runtime { + Runtime::new().expect("runtime should build") + } + + fn write_server_settings(path: &std::path::Path, destination: &str) { + std::fs::write( + path, + format!( + r#" +_version = 1 + +[server.auth] +methods = ["dev-token"] + +[server.logging] +destination = "{destination}" +"# + ), + ) + .expect("settings fixture should write"); + } + + fn serve_args_with_config(config_path: &std::path::Path) -> ServeArgs { + ServeArgs { + bind: Some("127.0.0.1:0".to_string()), + web: false, + no_web: false, + model: None, + provider: None, + sandbox: None, + max_concurrent_runs: None, + config: Some(config_path.to_path_buf()), + #[cfg(debug_assertions)] + watch_web: false, + } + } #[test] fn ensure_server_running_for_storage_errors_when_install_mode_is_required() { @@ -516,4 +571,87 @@ mod tests { "unexpected error: {message}" ); } + + #[test] + fn prepare_foreground_server_log_with_stdout_does_not_create_server_log() { + let storage_dir = tempfile::tempdir().unwrap(); + let runtime_directory = fabro_config::RuntimeDirectory::new(storage_dir.path()); + + let _bootstrap = runtime() + .block_on(prepare_foreground_server_log( + &runtime_directory, + &ServerLogDestination::Stdout, + )) + .expect("stdout foreground bootstrap should succeed"); + + assert!( + !runtime_directory.log_path().exists(), + "stdout destination should not create server.log" + ); + } + + #[test] + fn execute_daemon_rejects_configured_stdout_before_creating_server_log() { + let storage_dir = tempfile::tempdir().unwrap(); + let config_dir = tempfile::tempdir().unwrap(); + let config_path = config_dir.path().join("settings.toml"); + write_server_settings(&config_path, "stdout"); + + let bind = BindRequest::Tcp("127.0.0.1:0".parse().unwrap()); + let serve_args = serve_args_with_config(&config_path); + let err = runtime() + .block_on(execute_daemon( + &bind, + &serve_args, + storage_dir.path(), + false, + None, + Printer::Silent, + )) + .expect_err("daemon mode should reject stdout logging"); + + assert!( + err.to_string().contains("incompatible with daemon mode"), + "unexpected error: {err}" + ); + let runtime_directory = fabro_config::RuntimeDirectory::new(storage_dir.path()); + assert!( + !runtime_directory.log_path().exists(), + "daemon rejection should happen before server.log is created" + ); + } + + #[test] + fn execute_daemon_rejects_invalid_env_destination_before_creating_server_log() { + let storage_dir = tempfile::tempdir().unwrap(); + let config_dir = tempfile::tempdir().unwrap(); + let config_path = config_dir.path().join("settings.toml"); + write_server_settings(&config_path, "file"); + + let bind = BindRequest::Tcp("127.0.0.1:0".parse().unwrap()); + let serve_args = serve_args_with_config(&config_path); + + with_var(EnvVars::FABRO_LOG_DESTINATION, Some("stdot"), || { + let err = runtime() + .block_on(execute_daemon( + &bind, + &serve_args, + storage_dir.path(), + false, + None, + Printer::Silent, + )) + .expect_err("daemon mode should reject invalid env destination"); + + let message = err.to_string(); + assert!(message.contains(EnvVars::FABRO_LOG_DESTINATION)); + assert!(message.contains("stdot")); + }); + + let runtime_directory = fabro_config::RuntimeDirectory::new(storage_dir.path()); + assert!( + !runtime_directory.log_path().exists(), + "invalid env rejection should happen before server.log is created" + ); + } } diff --git a/lib/crates/fabro-cli/src/local_server.rs b/lib/crates/fabro-cli/src/local_server.rs index 5b15c8690..963f46ac5 100644 --- a/lib/crates/fabro-cli/src/local_server.rs +++ b/lib/crates/fabro-cli/src/local_server.rs @@ -7,15 +7,17 @@ use fabro_config::bind::BindRequest; use fabro_config::user::default_storage_dir; use fabro_server::serve::resolve_bind_request_from_server_settings; use fabro_types::ServerSettings; +use fabro_types::settings::server::LogDestination; use fabro_types::settings::{InterpString, ServerAuthMethod}; use crate::user_config; pub(crate) struct LocalServerConfig { - storage_dir: PathBuf, - auth_methods: Vec, - config_log_level: Option, - server_settings: std::result::Result, + storage_dir: PathBuf, + auth_methods: Vec, + config_log_level: Option, + config_log_destination: Option, + server_settings: std::result::Result, } impl LocalServerConfig { @@ -39,6 +41,7 @@ impl LocalServerConfig { storage_dir: settings.storage_dir, auth_methods, config_log_level: settings.config_log_level, + config_log_destination: settings.config_log_destination, server_settings, } } @@ -52,7 +55,13 @@ impl LocalServerConfig { } pub(crate) fn config_log_level(&self) -> Option<&str> { - self.config_log_level.as_deref() + self.config_log_level + .as_ref() + .map(fabro_config::LogFilter::as_str) + } + + pub(crate) fn config_log_destination(&self) -> Option { + self.config_log_destination } pub(crate) fn bind_request(&self, cli_override: Option<&str>) -> Result { diff --git a/lib/crates/fabro-cli/src/logging.rs b/lib/crates/fabro-cli/src/logging.rs index d0ec8b9b1..6119702e7 100644 --- a/lib/crates/fabro-cli/src/logging.rs +++ b/lib/crates/fabro-cli/src/logging.rs @@ -3,9 +3,11 @@ reason = "CLI logging setup: sync directory scan during startup" )] use std::fs::{File, OpenOptions}; -use std::path::Path; +use std::path::{Path, PathBuf}; use anyhow::{Context, Result}; +use fabro_static::EnvVars; +use fabro_types::settings::server::LogDestination; use fabro_util::run_log; use tracing_appender::rolling; use tracing_subscriber::fmt::writer::MakeWriter; @@ -18,7 +20,13 @@ const LOG_RETENTION_DAYS: u32 = 7; #[derive(Clone, Debug, PartialEq, Eq)] pub(crate) enum InternalLogSink { Cli, - Server { path: std::path::PathBuf }, + Server { destination: ServerLogDestination }, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub(crate) enum ServerLogDestination { + File(PathBuf), + Stdout, } pub(crate) fn init_tracing( @@ -31,8 +39,8 @@ pub(crate) fn init_tracing( } else { config_log_level.unwrap_or("info") }; - let filter = - EnvFilter::try_from_env("FABRO_LOG").unwrap_or_else(|_| EnvFilter::new(default_level)); + let filter = EnvFilter::try_from_env(EnvVars::FABRO_LOG) + .unwrap_or_else(|_| EnvFilter::new(default_level)); match sink { InternalLogSink::Cli => { @@ -52,14 +60,51 @@ pub(crate) fn init_tracing( cleanup_old_logs(&log_dir, "cli", LOG_RETENTION_DAYS); init_subscriber(filter, file_appender); } - InternalLogSink::Server { path } => { - init_subscriber(filter, FixedFileAppender::open(path)?); - } + InternalLogSink::Server { destination } => match destination { + ServerLogDestination::File(path) => { + init_subscriber(filter, FixedFileAppender::open(path)?); + } + ServerLogDestination::Stdout => { + init_subscriber(filter, std::io::stdout); + } + }, } Ok(()) } +pub(crate) fn resolve_log_destination( + config_destination: LogDestination, +) -> Result { + let env_value = std::env::var(EnvVars::FABRO_LOG_DESTINATION).ok(); + resolve_log_destination_with_env(config_destination, env_value.as_deref()) +} + +pub(crate) fn resolve_log_destination_with_env( + config_destination: LogDestination, + env_value: Option<&str>, +) -> Result { + match env_value { + Some(value) => value.parse::().with_context(|| { + format!( + "invalid {} value `{value}`; expected `file` or `stdout`", + EnvVars::FABRO_LOG_DESTINATION + ) + }), + None => Ok(config_destination), + } +} + +pub(crate) fn server_log_destination( + destination: LogDestination, + log_path: PathBuf, +) -> ServerLogDestination { + match destination { + LogDestination::File => ServerLogDestination::File(log_path), + LogDestination::Stdout => ServerLogDestination::Stdout, + } +} + fn cleanup_old_logs(log_dir: &Path, prefix: &str, max_age_days: u32) { let cutoff = chrono::Utc::now().date_naive() - chrono::Duration::days(i64::from(max_age_days)); let Ok(entries) = std::fs::read_dir(log_dir) else { diff --git a/lib/crates/fabro-cli/src/main.rs b/lib/crates/fabro-cli/src/main.rs index 949ab7b90..9cb2b7e55 100644 --- a/lib/crates/fabro-cli/src/main.rs +++ b/lib/crates/fabro-cli/src/main.rs @@ -461,15 +461,26 @@ async fn prepare_server_bootstrap( let local_config = local_server::LocalServerConfig::load(config_path, storage_dir)?; let storage_dir = local_config.storage_dir().to_path_buf(); let runtime_directory = fabro_config::RuntimeDirectory::new(storage_dir.clone()); + let log_destination = logging::resolve_log_destination( + local_config.config_log_destination().unwrap_or_default(), + )?; + let server_log_destination = + logging::server_log_destination(log_destination, runtime_directory.log_path()); let foreground_server_log_bootstrap = if foreground { - Some(commands::server::start::prepare_foreground_server_log(&runtime_directory).await?) + Some( + commands::server::start::prepare_foreground_server_log( + &runtime_directory, + &server_log_destination, + ) + .await?, + ) } else { None }; Ok(PreTracingBootstrap { sink: logging::InternalLogSink::Server { - path: runtime_directory.log_path(), + destination: server_log_destination, }, config_log_level: local_config.config_log_level().map(str::to_owned), foreground_server_log_bootstrap, @@ -486,6 +497,7 @@ mod tests { AuthCommand, AuthNamespace, Commands, InstallGitHubStrategyArg, ModelsCommand, ProviderCommand, ProviderNamespace, }; + use temp_env::with_var; use tokio::runtime::Runtime; use super::*; @@ -495,18 +507,32 @@ mod tests { } fn write_test_settings(path: &std::path::Path) { + write_test_settings_with_logging(path, "warn", "stdout"); + } + + fn write_test_settings_with_logging(path: &std::path::Path, level: &str, destination: &str) { std::fs::write( path, - r#" + format!( + r#" _version = 1 [server.logging] -level = "warn" -"#, +level = "{level}" +destination = "{destination}" +"# + ), ) .unwrap(); } + fn expect_bootstrap_err(result: Result) -> anyhow::Error { + match result { + Ok(_) => panic!("bootstrap should have failed"), + Err(err) => err, + } + } + #[test] fn pre_tracing_bootstrap_uses_cli_sink_for_normal_cli_command() { let cli = Cli::try_parse_from(["fabro", "uninstall"]).expect("should parse"); @@ -665,7 +691,7 @@ level = "warn" .expect("bootstrap should resolve"); assert_eq!(bootstrap.sink, logging::InternalLogSink::Server { - path: storage_dir.path().join("logs").join("server.log"), + destination: logging::ServerLogDestination::Stdout, }); assert_eq!(bootstrap.config_log_level.as_deref(), Some("warn")); assert!(bootstrap.foreground_server_log_bootstrap.is_some()); @@ -696,7 +722,7 @@ level = "warn" .expect("bootstrap should resolve"); assert_eq!(bootstrap.sink, logging::InternalLogSink::Server { - path: storage_dir.path().join("logs").join("server.log"), + destination: logging::ServerLogDestination::Stdout, }); assert_eq!(bootstrap.config_log_level.as_deref(), Some("warn")); assert!(bootstrap.foreground_server_log_bootstrap.is_some()); @@ -725,12 +751,125 @@ level = "warn" .expect("bootstrap should resolve"); assert_eq!(bootstrap.sink, logging::InternalLogSink::Server { - path: storage_dir.path().join("logs").join("server.log"), + destination: logging::ServerLogDestination::Stdout, }); assert_eq!(bootstrap.config_log_level.as_deref(), Some("warn")); assert!(bootstrap.foreground_server_log_bootstrap.is_none()); } + #[test] + fn pre_tracing_bootstrap_env_destination_overrides_config_file() { + let storage_dir = tempfile::tempdir().unwrap(); + let config_dir = tempfile::tempdir().unwrap(); + let config_path = config_dir.path().join("settings.toml"); + write_test_settings_with_logging(&config_path, "warn", "file"); + + let cli = Cli::try_parse_from([ + "fabro", + "server", + "start", + "--foreground", + "--storage-dir", + storage_dir.path().to_str().unwrap(), + "--config", + config_path.to_str().unwrap(), + ]) + .expect("should parse"); + let command = cli.command.as_deref().unwrap(); + + with_var(EnvVars::FABRO_LOG_DESTINATION, Some("stdout"), || { + let bootstrap = runtime() + .block_on(pre_tracing_bootstrap(command)) + .expect("bootstrap should resolve"); + + assert_eq!(bootstrap.sink, logging::InternalLogSink::Server { + destination: logging::ServerLogDestination::Stdout, + }); + }); + } + + #[test] + fn pre_tracing_bootstrap_rejects_invalid_env_destination() { + let storage_dir = tempfile::tempdir().unwrap(); + let config_dir = tempfile::tempdir().unwrap(); + let config_path = config_dir.path().join("settings.toml"); + write_test_settings_with_logging(&config_path, "warn", "file"); + + let cli = Cli::try_parse_from([ + "fabro", + "server", + "start", + "--foreground", + "--storage-dir", + storage_dir.path().to_str().unwrap(), + "--config", + config_path.to_str().unwrap(), + ]) + .expect("should parse"); + let command = cli.command.as_deref().unwrap(); + + with_var(EnvVars::FABRO_LOG_DESTINATION, Some("stdot"), || { + let err = expect_bootstrap_err(runtime().block_on(pre_tracing_bootstrap(command))); + let message = err.to_string(); + assert!(message.contains(EnvVars::FABRO_LOG_DESTINATION)); + assert!(message.contains("stdot")); + }); + } + + #[test] + fn pre_tracing_bootstrap_rejects_invalid_config_log_level() { + let storage_dir = tempfile::tempdir().unwrap(); + let config_dir = tempfile::tempdir().unwrap(); + let config_path = config_dir.path().join("settings.toml"); + write_test_settings_with_logging(&config_path, "definitely not a filter", "file"); + + let cli = Cli::try_parse_from([ + "fabro", + "server", + "start", + "--foreground", + "--storage-dir", + storage_dir.path().to_str().unwrap(), + "--config", + config_path.to_str().unwrap(), + ]) + .expect("should parse"); + let command = cli.command.as_deref().unwrap(); + + let err = expect_bootstrap_err(runtime().block_on(pre_tracing_bootstrap(command))); + assert!( + err.to_string().contains("server.logging.level"), + "unexpected error: {err}" + ); + } + + #[test] + fn pre_tracing_bootstrap_rejects_invalid_config_destination() { + let storage_dir = tempfile::tempdir().unwrap(); + let config_dir = tempfile::tempdir().unwrap(); + let config_path = config_dir.path().join("settings.toml"); + write_test_settings_with_logging(&config_path, "warn", "stdot"); + + let cli = Cli::try_parse_from([ + "fabro", + "server", + "start", + "--foreground", + "--storage-dir", + storage_dir.path().to_str().unwrap(), + "--config", + config_path.to_str().unwrap(), + ]) + .expect("should parse"); + let command = cli.command.as_deref().unwrap(); + + let err = expect_bootstrap_err(runtime().block_on(pre_tracing_bootstrap(command))); + assert!( + err.to_string().contains("server.logging.destination"), + "unexpected error: {err}" + ); + } + #[test] fn pre_tracing_bootstrap_uses_cli_sink_for_server_start_daemon_wrapper() { let storage_dir = tempfile::tempdir().unwrap(); diff --git a/lib/crates/fabro-cli/src/user_config.rs b/lib/crates/fabro-cli/src/user_config.rs index d6bf4bec9..55bb1cb7e 100644 --- a/lib/crates/fabro-cli/src/user_config.rs +++ b/lib/crates/fabro-cli/src/user_config.rs @@ -1,15 +1,16 @@ use std::path::{Path, PathBuf}; use std::str::FromStr; -use anyhow::Result; +use anyhow::{Context, Result, anyhow}; pub(crate) use fabro_client::ServerTarget; pub(crate) use fabro_config::user::{active_settings_path, default_storage_dir}; use fabro_config::user::{default_settings_path, default_socket_path}; use fabro_config::{ - CliLayer, ParseError, RunSettingsBuilder, ServerSettingsBuilder, UserSettingsBuilder, + CliLayer, LogFilter, ParseError, RunSettingsBuilder, ServerSettingsBuilder, UserSettingsBuilder, }; use fabro_static::EnvVars; use fabro_types::settings::cli::CliTargetSettings; +use fabro_types::settings::server::LogDestination; use fabro_types::settings::{CliNamespace, InterpString, RunNamespace}; use fabro_types::{ServerSettings, UserSettings}; use fabro_util::version::FABRO_VERSION; @@ -18,11 +19,12 @@ use tracing::debug; use crate::args::ServerTargetArgs; pub(crate) struct LoadedSettings { - pub(crate) storage_dir: PathBuf, - pub(crate) config_log_level: Option, - pub(crate) run_settings: std::result::Result, - pub(crate) server_settings: std::result::Result, - pub(crate) user_settings: UserSettings, + pub(crate) storage_dir: PathBuf, + pub(crate) config_log_level: Option, + pub(crate) config_log_destination: Option, + pub(crate) run_settings: std::result::Result, + pub(crate) server_settings: std::result::Result, + pub(crate) user_settings: UserSettings, } pub(crate) fn load_resolved_settings( @@ -33,7 +35,7 @@ pub(crate) fn load_resolved_settings( let document = load_settings_document(config_path)?; let storage_override = storage_dir.map(Path::to_path_buf); let storage_dir = storage_dir_from_document(&document, storage_dir)?; - let config_log_level = config_log_level_from_document(&document); + let pre_tracing_config = pre_tracing_config_from_document(&document)?; let run_settings = load_run_settings(config_path).map_err(|err| err.to_string()); let server_settings = load_server_settings(config_path) .map(|settings| match storage_override.as_deref() { @@ -45,7 +47,8 @@ pub(crate) fn load_resolved_settings( Ok(LoadedSettings { storage_dir, - config_log_level, + config_log_level: pre_tracing_config.log_level, + config_log_destination: pre_tracing_config.log_destination, run_settings, server_settings, user_settings, @@ -118,8 +121,48 @@ fn load_user_settings( }) } -fn config_log_level_from_document(document: &toml::Value) -> Option { - string_at_path(document, &["server", "logging", "level"]) +struct PreTracingConfig { + log_level: Option, + log_destination: Option, +} + +fn pre_tracing_config_from_document(document: &toml::Value) -> Result { + Ok(PreTracingConfig { + log_level: log_filter_at_path(document, &["server", "logging", "level"])?, + log_destination: log_destination_at_path(document, &["server", "logging", "destination"])?, + }) +} + +fn log_filter_at_path(document: &toml::Value, path: &[&str]) -> Result> { + let Some(value) = value_at_path(document, path) else { + return Ok(None); + }; + let raw = value + .as_str() + .ok_or_else(|| anyhow!("{} must be a string", path.join(".")))?; + LogFilter::parse(raw) + .with_context(|| format!("invalid {} `{raw}`", path.join("."))) + .map(Some) +} + +fn log_destination_at_path( + document: &toml::Value, + path: &[&str], +) -> Result> { + let Some(value) = value_at_path(document, path) else { + return Ok(None); + }; + let raw = value + .as_str() + .ok_or_else(|| anyhow!("{} must be a string", path.join(".")))?; + raw.parse::() + .with_context(|| { + format!( + "invalid {} `{raw}`; expected `file` or `stdout`", + path.join(".") + ) + }) + .map(Some) } fn storage_dir_from_document( @@ -163,11 +206,15 @@ fn storage_dir_from_document_with_lookup( } fn string_at_path(document: &toml::Value, path: &[&str]) -> Option { + value_at_path(document, path).and_then(|value| value.as_str().map(str::to_owned)) +} + +fn value_at_path<'a>(document: &'a toml::Value, path: &[&str]) -> Option<&'a toml::Value> { let mut current = document; for segment in path { current = current.get(*segment)?; } - current.as_str().map(str::to_owned) + Some(current) } /// Pull the resolved CLI target configuration out of `[cli.target]`. @@ -233,7 +280,7 @@ pub(crate) fn load_resolved_settings_from_toml( .map_err(|err| anyhow::anyhow!("failed to parse settings file: {err}"))?; let storage_override = storage_dir.map(Path::to_path_buf); let storage_dir = storage_dir_from_document(&document, storage_dir)?; - let config_log_level = config_log_level_from_document(&document); + let pre_tracing_config = pre_tracing_config_from_document(&document)?; let run_settings = RunSettingsBuilder::from_toml(source).map_err(|err| err.to_string()); let server_settings = ServerSettingsBuilder::from_toml(source) .map(|settings| match storage_override.as_deref() { @@ -248,7 +295,8 @@ pub(crate) fn load_resolved_settings_from_toml( Ok(LoadedSettings { storage_dir, - config_log_level, + config_log_level: pre_tracing_config.log_level, + config_log_destination: pre_tracing_config.log_destination, run_settings, server_settings, user_settings, diff --git a/lib/crates/fabro-config/Cargo.toml b/lib/crates/fabro-config/Cargo.toml index f950b63de..1eb3b7247 100644 --- a/lib/crates/fabro-config/Cargo.toml +++ b/lib/crates/fabro-config/Cargo.toml @@ -34,6 +34,7 @@ strsim = "0.11" tempfile = "3" toml.workspace = true tracing.workspace = true +tracing-subscriber.workspace = true thiserror.workspace = true ulid.workspace = true diff --git a/lib/crates/fabro-config/src/layers/combine.rs b/lib/crates/fabro-config/src/layers/combine.rs index 90f2dec14..1cbebf123 100644 --- a/lib/crates/fabro-config/src/layers/combine.rs +++ b/lib/crates/fabro-config/src/layers/combine.rs @@ -5,10 +5,12 @@ use fabro_types::settings::run::{ AgentPermissions, ApprovalMode, DaytonaNetworkLayer, MergeStrategy, RunMode, WorktreeMode, }; use fabro_types::settings::server::{ - GithubIntegrationStrategy, ObjectStoreProvider, ServerAuthMethod, WebhookStrategy, + GithubIntegrationStrategy, LogDestination, ObjectStoreProvider, ServerAuthMethod, + WebhookStrategy, }; use fabro_types::settings::{Duration, InterpString, Size}; +use super::LogFilter; use super::cli::{CliAuthLayer, CliLoggingLayer, CliTargetLayer}; use super::features::FeaturesLayer; use super::run::{ @@ -18,7 +20,7 @@ use super::run::{ }; use super::server::{ ObjectStoreLocalLayer, ObjectStoreS3Layer, ServerApiLayer, ServerAuthGithubLayer, - ServerListenLayer, ServerLoggingLayer, + ServerListenLayer, }; /// Internal merge trait used by sparse config layers inside `fabro-config`. @@ -74,9 +76,11 @@ impl_combine_or_option!( RunMode, WorktreeMode, GithubIntegrationStrategy, + LogDestination, ObjectStoreProvider, ServerAuthMethod, WebhookStrategy, + LogFilter, ); impl Combine for Option> { @@ -128,7 +132,6 @@ impl_combine_self!( ServerApiLayer, ServerAuthGithubLayer, ServerListenLayer, - ServerLoggingLayer, ); impl Combine for RunCheckpointLayer { @@ -284,8 +287,13 @@ mod tests { GithubIntegrationStrategy::Token, ); assert_option_leaf(ObjectStoreProvider::S3, ObjectStoreProvider::Local); + assert_option_leaf(LogDestination::Stdout, LogDestination::File); assert_option_leaf(ServerAuthMethod::Github, ServerAuthMethod::DevToken); assert_option_leaf(WebhookStrategy::ServerUrl, WebhookStrategy::TailscaleFunnel); + assert_option_leaf( + LogFilter::parse("debug").unwrap(), + LogFilter::parse("info").unwrap(), + ); assert_option_leaf(vec!["this".to_string()], vec!["fallback".to_string()]); assert_option_leaf(vec![ServerAuthMethod::Github], vec![ ServerAuthMethod::DevToken, diff --git a/lib/crates/fabro-config/src/layers/log_filter.rs b/lib/crates/fabro-config/src/layers/log_filter.rs new file mode 100644 index 000000000..4ec82a1e8 --- /dev/null +++ b/lib/crates/fabro-config/src/layers/log_filter.rs @@ -0,0 +1,45 @@ +use serde::de::Error as _; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use tracing_subscriber::EnvFilter; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct LogFilter(String); + +impl LogFilter { + pub fn parse(value: &str) -> anyhow::Result { + if value.chars().any(char::is_whitespace) { + anyhow::bail!("filter must not contain whitespace"); + } + + EnvFilter::builder() + .parse(value) + .map(|_| Self(value.to_owned())) + .map_err(Into::into) + } + + #[must_use] + pub fn as_str(&self) -> &str { + &self.0 + } +} + +impl Serialize for LogFilter { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(&self.0) + } +} + +impl<'de> Deserialize<'de> for LogFilter { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let value = String::deserialize(deserializer)?; + Self::parse(&value).map_err(|err| { + D::Error::custom(format!("invalid server.logging.level `{value}`: {err}")) + }) + } +} diff --git a/lib/crates/fabro-config/src/layers/mod.rs b/lib/crates/fabro-config/src/layers/mod.rs index 331c6a67c..233aa952b 100644 --- a/lib/crates/fabro-config/src/layers/mod.rs +++ b/lib/crates/fabro-config/src/layers/mod.rs @@ -1,6 +1,7 @@ mod cli; mod combine; mod features; +mod log_filter; mod maps; mod project; mod run; @@ -15,6 +16,7 @@ pub use cli::{ }; pub(crate) use combine::Combine; pub use features::FeaturesLayer; +pub use log_filter::LogFilter; pub use maps::{MergeMap, ReplaceMap, StickyMap}; pub use project::ProjectLayer; pub use run::{ diff --git a/lib/crates/fabro-config/src/layers/server.rs b/lib/crates/fabro-config/src/layers/server.rs index 8b4d30e02..61560e313 100644 --- a/lib/crates/fabro-config/src/layers/server.rs +++ b/lib/crates/fabro-config/src/layers/server.rs @@ -1,11 +1,13 @@ //! Sparse `[server]` settings layer definitions. use fabro_types::settings::server::{ - GithubIntegrationStrategy, ObjectStoreProvider, ServerAuthMethod, WebhookStrategy, + GithubIntegrationStrategy, LogDestination, ObjectStoreProvider, ServerAuthMethod, + WebhookStrategy, }; use fabro_types::settings::{Duration, InterpString}; use serde::{Deserialize, Serialize}; +use super::LogFilter; use super::maps::StickyMap; #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, fabro_macros::Combine)] @@ -179,11 +181,13 @@ pub struct ServerSchedulerLayer { } /// `[server.logging]` — process-owned logging configuration for the server. -#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] +#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, fabro_macros::Combine)] #[serde(deny_unknown_fields)] pub struct ServerLoggingLayer { #[serde(default, skip_serializing_if = "Option::is_none")] - pub level: Option, + pub level: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub destination: Option, } /// `[server.integrations.]` — cohesive integration surface for chat diff --git a/lib/crates/fabro-config/src/lib.rs b/lib/crates/fabro-config/src/lib.rs index c02ffa4f7..a5edd51a2 100644 --- a/lib/crates/fabro-config/src/lib.rs +++ b/lib/crates/fabro-config/src/lib.rs @@ -39,7 +39,7 @@ pub use layers::{ CliOutputLayer, CliTargetLayer, CliUpdatesLayer, DaytonaDockerfileLayer, DaytonaSandboxLayer, DaytonaSnapshotLayer, DiscordIntegrationLayer, FeaturesLayer, GitAuthorLayer, GithubIntegrationLayer, HookAgentMarker, HookEntry, HookTlsMode, IntegrationWebhooksLayer, - InterviewProviderLayer, InterviewsLayer, LocalSandboxLayer, McpEntryLayer, MergeMap, + InterviewProviderLayer, InterviewsLayer, LocalSandboxLayer, LogFilter, McpEntryLayer, MergeMap, ModelRefOrSplice, NotificationProviderLayer, NotificationRouteLayer, ObjectStoreLocalLayer, ObjectStoreS3Layer, PrepareStep, ProjectLayer, ReplaceMap, RunAgentLayer, RunArtifactsLayer, RunCheckpointLayer, RunExecutionLayer, RunGitLayer, RunGoalLayer, RunLayer, RunModelLayer, diff --git a/lib/crates/fabro-config/src/resolve/server.rs b/lib/crates/fabro-config/src/resolve/server.rs index eeb8371ca..6805fd6bd 100644 --- a/lib/crates/fabro-config/src/resolve/server.rs +++ b/lib/crates/fabro-config/src/resolve/server.rs @@ -49,10 +49,16 @@ pub fn resolve_server(layer: &ServerLayer, errors: &mut Vec) -> Se .expect("defaults.toml should provide server.scheduler.max_concurrent_runs"), }, logging: ServerLoggingSettings { - level: layer + level: layer .logging .as_ref() - .and_then(|logging| logging.level.clone()), + .and_then(|logging| logging.level.as_ref()) + .map(|level| level.as_str().to_owned()), + destination: layer + .logging + .as_ref() + .and_then(|logging| logging.destination) + .unwrap_or_default(), }, integrations, } diff --git a/lib/crates/fabro-config/src/tests/combine.rs b/lib/crates/fabro-config/src/tests/combine.rs index be3677265..a3576dd50 100644 --- a/lib/crates/fabro-config/src/tests/combine.rs +++ b/lib/crates/fabro-config/src/tests/combine.rs @@ -1,5 +1,6 @@ use fabro_types::settings::InterpString; use fabro_types::settings::cli::{OutputFormat, OutputVerbosity}; +use fabro_types::settings::server::LogDestination; use crate::{Combine, SettingsLayer, StringOrSplice}; @@ -259,6 +260,30 @@ level = "debug" assert_eq!(updates.check, Some(true)); } +#[test] +fn server_logging_merges_by_field() { + let lower = parse( + r#" +[server.logging] +level = "warn" +"#, + ); + let higher = parse( + r#" +[server.logging] +destination = "stdout" +"#, + ); + + let merged = higher.combine(lower); + let logging = merged.server.unwrap().logging.unwrap(); + assert_eq!( + logging.level.as_ref().map(fabro_config::LogFilter::as_str), + Some("warn") + ); + assert_eq!(logging.destination, Some(LogDestination::Stdout)); +} + #[test] fn whole_replace_option_subtable_does_not_inherit_fallback_fields() { let lower = parse( diff --git a/lib/crates/fabro-config/src/tests/log_filter.rs b/lib/crates/fabro-config/src/tests/log_filter.rs new file mode 100644 index 000000000..e1da6381f --- /dev/null +++ b/lib/crates/fabro-config/src/tests/log_filter.rs @@ -0,0 +1,28 @@ +use fabro_config::LogFilter; + +#[test] +fn log_filter_accepts_env_filter_directives() { + let filter = LogFilter::parse("info,fabro_server=debug") + .expect("valid env filter directive should parse"); + + assert_eq!(filter.as_str(), "info,fabro_server=debug"); +} + +#[test] +fn log_filter_rejects_invalid_directives() { + LogFilter::parse("definitely not a filter") + .expect_err("whitespace phrase should not be accepted as a filter"); + LogFilter::parse("fabro_server=definitelynotalevel") + .expect_err("unknown level should not be accepted as a filter"); +} + +#[test] +fn log_filter_round_trips_through_serde() { + let filter = LogFilter::parse("warn,fabro_cli=debug").unwrap(); + + let json = serde_json::to_value(&filter).expect("filter should serialize"); + assert_eq!(json, "warn,fabro_cli=debug"); + + let round_trip: LogFilter = serde_json::from_value(json).expect("filter should deserialize"); + assert_eq!(round_trip, filter); +} diff --git a/lib/crates/fabro-config/src/tests/mod.rs b/lib/crates/fabro-config/src/tests/mod.rs index 9e1d8122b..593af6efc 100644 --- a/lib/crates/fabro-config/src/tests/mod.rs +++ b/lib/crates/fabro-config/src/tests/mod.rs @@ -1,5 +1,6 @@ mod combine; mod defaults; +mod log_filter; mod resolve_cli; mod resolve_features; mod resolve_project; diff --git a/lib/crates/fabro-config/src/tests/resolve_server.rs b/lib/crates/fabro-config/src/tests/resolve_server.rs index febec8178..a306a9800 100644 --- a/lib/crates/fabro-config/src/tests/resolve_server.rs +++ b/lib/crates/fabro-config/src/tests/resolve_server.rs @@ -5,7 +5,7 @@ use fabro_types::settings::InterpString; use fabro_types::settings::server::{ - GithubIntegrationStrategy, IpAllowEntry, ObjectStoreSettings, ServerAuthMethod, + GithubIntegrationStrategy, IpAllowEntry, LogDestination, ObjectStoreSettings, ServerAuthMethod, ServerListenSettings, ServerNamespace, }; use fabro_util::Home; @@ -67,6 +67,7 @@ fn resolves_server_defaults_from_empty_settings() { assert!(settings.web.enabled); assert_eq!(settings.web.url.as_source(), "http://localhost:3000"); assert_eq!(settings.scheduler.max_concurrent_runs, 5); + assert_eq!(settings.logging.destination, LogDestination::File); match settings.listen { ServerListenSettings::Unix { path } => { @@ -108,6 +109,39 @@ fn resolves_server_defaults_from_empty_settings() { assert!(!settings.slatedb.disk_cache); } +#[test] +fn resolves_server_logging_destination_from_settings() { + let file = parse( + r#" +_version = 1 + +[server.logging] +destination = "stdout" +"#, + ); + + let settings = resolve_server(&file); + + assert_eq!(settings.logging.destination, LogDestination::Stdout); +} + +#[test] +fn parsing_rejects_invalid_server_log_filter() { + let err = r#" +_version = 1 + +[server.logging] +level = "definitely not a filter" +"# + .parse::() + .expect_err("invalid log filters should be rejected at parse time"); + + assert!( + err.to_string().contains("server.logging.level"), + "unexpected error: {err}" + ); +} + #[test] fn server_settings_from_layer_matches_namespace_resolvers() { let settings = parse( diff --git a/lib/crates/fabro-static/src/env_vars.rs b/lib/crates/fabro-static/src/env_vars.rs index 30e8594a0..6688b6f17 100644 --- a/lib/crates/fabro-static/src/env_vars.rs +++ b/lib/crates/fabro-static/src/env_vars.rs @@ -19,6 +19,8 @@ impl EnvVars { pub const FABRO_HOME: &'static str = "FABRO_HOME"; pub const FABRO_HTTP_PROXY_POLICY: &'static str = "FABRO_HTTP_PROXY_POLICY"; pub const FABRO_JSON: &'static str = "FABRO_JSON"; + pub const FABRO_LOG: &'static str = "FABRO_LOG"; + pub const FABRO_LOG_DESTINATION: &'static str = "FABRO_LOG_DESTINATION"; pub const FABRO_NO_UPGRADE_CHECK: &'static str = "FABRO_NO_UPGRADE_CHECK"; pub const FABRO_QUIET: &'static str = "FABRO_QUIET"; pub const FABRO_SERVER: &'static str = "FABRO_SERVER"; @@ -134,6 +136,8 @@ mod tests { #[test] fn env_var_constants_match_their_names() { assert_eq!(EnvVars::FABRO_CONFIG, "FABRO_CONFIG"); + assert_eq!(EnvVars::FABRO_LOG, "FABRO_LOG"); + assert_eq!(EnvVars::FABRO_LOG_DESTINATION, "FABRO_LOG_DESTINATION"); } #[test] @@ -151,6 +155,8 @@ mod tests { EnvVars::FABRO_HOME, EnvVars::FABRO_HTTP_PROXY_POLICY, EnvVars::FABRO_JSON, + EnvVars::FABRO_LOG, + EnvVars::FABRO_LOG_DESTINATION, EnvVars::FABRO_NO_UPGRADE_CHECK, EnvVars::FABRO_QUIET, EnvVars::FABRO_SERVER, diff --git a/lib/crates/fabro-types/src/settings/mod.rs b/lib/crates/fabro-types/src/settings/mod.rs index 76a45cea7..9b3b6aa31 100644 --- a/lib/crates/fabro-types/src/settings/mod.rs +++ b/lib/crates/fabro-types/src/settings/mod.rs @@ -45,7 +45,7 @@ pub use run::{ }; pub use server::{ DiscordIntegrationSettings, GithubIntegrationSettings, IntegrationWebhooksSettings, - IpAllowEntry, ObjectStoreSettings, ServerApiSettings, ServerArtifactsSettings, + IpAllowEntry, LogDestination, ObjectStoreSettings, ServerApiSettings, ServerArtifactsSettings, ServerAuthGithubSettings, ServerAuthMethod, ServerAuthSettings, ServerIntegrationsSettings, ServerIpAllowlistOverrideSettings, ServerIpAllowlistSettings, ServerListenSettings, ServerLoggingSettings, ServerNamespace, ServerSchedulerSettings, ServerSlateDbSettings, diff --git a/lib/crates/fabro-types/src/settings/server.rs b/lib/crates/fabro-types/src/settings/server.rs index a80937d78..fa7ed1626 100644 --- a/lib/crates/fabro-types/src/settings/server.rs +++ b/lib/crates/fabro-types/src/settings/server.rs @@ -230,9 +230,20 @@ pub struct ServerSchedulerSettings { pub max_concurrent_runs: usize, } +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, strum::EnumString)] +#[serde(rename_all = "lowercase")] +#[strum(serialize_all = "lowercase")] +pub enum LogDestination { + #[default] + File, + Stdout, +} + #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub struct ServerLoggingSettings { - pub level: Option, + pub level: Option, + #[serde(default)] + pub destination: LogDestination, } #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] diff --git a/lib/packages/fabro-api-client/src/.openapi-generator/FILES b/lib/packages/fabro-api-client/src/.openapi-generator/FILES index e76e82034..64256d2a4 100644 --- a/lib/packages/fabro-api-client/src/.openapi-generator/FILES +++ b/lib/packages/fabro-api-client/src/.openapi-generator/FILES @@ -103,6 +103,7 @@ models/integration-webhooks-settings.ts models/internal-stage-status.ts models/ip-allow-entry.ts models/literal-ip-allow-entry.ts +models/log-destination.ts models/manifest-args.ts models/manifest-config.ts models/manifest-file-entry.ts diff --git a/lib/packages/fabro-api-client/src/models/index.ts b/lib/packages/fabro-api-client/src/models/index.ts index 83d3be6ef..2bbcaf17c 100644 --- a/lib/packages/fabro-api-client/src/models/index.ts +++ b/lib/packages/fabro-api-client/src/models/index.ts @@ -82,6 +82,7 @@ export * from './integration-webhooks-settings'; export * from './internal-stage-status'; export * from './ip-allow-entry'; export * from './literal-ip-allow-entry'; +export * from './log-destination'; export * from './manifest-args'; export * from './manifest-config'; export * from './manifest-file-entry'; diff --git a/lib/packages/fabro-api-client/src/models/log-destination.ts b/lib/packages/fabro-api-client/src/models/log-destination.ts new file mode 100644 index 000000000..6e7caf66f --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/log-destination.ts @@ -0,0 +1,23 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Fabro Run API + * HTTP API for managing Fabro workflow run executions. + * + * The version of the OpenAPI document: 0.1.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + + + +export const LogDestination = { + FILE: 'file', + STDOUT: 'stdout' +} as const; + +export type LogDestination = typeof LogDestination[keyof typeof LogDestination]; diff --git a/lib/packages/fabro-api-client/src/models/server-logging-settings.ts b/lib/packages/fabro-api-client/src/models/server-logging-settings.ts index d9ab078f4..2fef328fd 100644 --- a/lib/packages/fabro-api-client/src/models/server-logging-settings.ts +++ b/lib/packages/fabro-api-client/src/models/server-logging-settings.ts @@ -5,7 +5,7 @@ * HTTP API for managing Fabro workflow run executions. * * The version of the OpenAPI document: 0.1.0 - * + * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech @@ -13,8 +13,11 @@ */ +// May contain unused imports in some cases +// @ts-ignore +import type { LogDestination } from './log-destination'; export interface ServerLoggingSettings { 'level': string | null; + 'destination': LogDestination; } -