diff --git a/src/Cli.Tests/EndToEndTests.cs b/src/Cli.Tests/EndToEndTests.cs index 99a9b77b6e..5b56a1d3c1 100644 --- a/src/Cli.Tests/EndToEndTests.cs +++ b/src/Cli.Tests/EndToEndTests.cs @@ -810,8 +810,9 @@ public Task TestUpdatingStoredProcedureWithRestMethods() /// /// Test to validate that the engine starts successfully when --verbose and --LogLevel - /// options are used with the start command - /// This test does not validate whether the engine logs messages at the specified log level + /// options are used with the start command, for log levels at or below Information. + /// CLI phase messages (version info, config path) are expected in the output because + /// they are logged at Information level. /// /// Log level options [DataTestMethod] @@ -820,24 +821,12 @@ public Task TestUpdatingStoredProcedureWithRestMethods() [DataRow("--LogLevel 0", DisplayName = "LogLevel 0 from command line.")] [DataRow("--LogLevel 1", DisplayName = "LogLevel 1 from command line.")] [DataRow("--LogLevel 2", DisplayName = "LogLevel 2 from command line.")] - [DataRow("--LogLevel 3", DisplayName = "LogLevel 3 from command line.")] - [DataRow("--LogLevel 4", DisplayName = "LogLevel 4 from command line.")] - [DataRow("--LogLevel 5", DisplayName = "LogLevel 5 from command line.")] - [DataRow("--LogLevel 6", DisplayName = "LogLevel 6 from command line.")] [DataRow("--LogLevel Trace", DisplayName = "LogLevel Trace from command line.")] [DataRow("--LogLevel Debug", DisplayName = "LogLevel Debug from command line.")] [DataRow("--LogLevel Information", DisplayName = "LogLevel Information from command line.")] - [DataRow("--LogLevel Warning", DisplayName = "LogLevel Warning from command line.")] - [DataRow("--LogLevel Error", DisplayName = "LogLevel Error from command line.")] - [DataRow("--LogLevel Critical", DisplayName = "LogLevel Critical from command line.")] - [DataRow("--LogLevel None", DisplayName = "LogLevel None from command line.")] [DataRow("--LogLevel tRace", DisplayName = "Case sensitivity: LogLevel Trace from command line.")] [DataRow("--LogLevel DebUG", DisplayName = "Case sensitivity: LogLevel Debug from command line.")] [DataRow("--LogLevel information", DisplayName = "Case sensitivity: LogLevel Information from command line.")] - [DataRow("--LogLevel waRNing", DisplayName = "Case sensitivity: LogLevel Warning from command line.")] - [DataRow("--LogLevel eRROR", DisplayName = "Case sensitivity: LogLevel Error from command line.")] - [DataRow("--LogLevel CrItIcal", DisplayName = "Case sensitivity: LogLevel Critical from command line.")] - [DataRow("--LogLevel NONE", DisplayName = "Case sensitivity: LogLevel None from command line.")] public void TestEngineStartUpWithVerboseAndLogLevelOptions(string logLevelOption) { _fileSystem!.File.WriteAllText(TEST_RUNTIME_CONFIG_FILE, INITIAL_CONFIG); @@ -856,6 +845,60 @@ public void TestEngineStartUpWithVerboseAndLogLevelOptions(string logLevelOption StringAssert.Contains(output, $"User provided config file: {TEST_RUNTIME_CONFIG_FILE}", StringComparison.Ordinal); } + /// + /// Test to validate that the engine starts successfully when --LogLevel is set to Warning + /// or above. At these levels, CLI phase messages (logged at Information) are suppressed, + /// so no stdout output is expected during the CLI phase. + /// + /// Log level options + [DataTestMethod] + [DataRow("--LogLevel 3", DisplayName = "LogLevel 3 from command line.")] + [DataRow("--LogLevel 4", DisplayName = "LogLevel 4 from command line.")] + [DataRow("--LogLevel 5", DisplayName = "LogLevel 5 from command line.")] + [DataRow("--LogLevel 6", DisplayName = "LogLevel 6 from command line.")] + [DataRow("--LogLevel Warning", DisplayName = "LogLevel Warning from command line.")] + [DataRow("--LogLevel Error", DisplayName = "LogLevel Error from command line.")] + [DataRow("--LogLevel Critical", DisplayName = "LogLevel Critical from command line.")] + [DataRow("--LogLevel None", DisplayName = "LogLevel None from command line.")] + [DataRow("--LogLevel waRNing", DisplayName = "Case sensitivity: LogLevel Warning from command line.")] + [DataRow("--LogLevel eRROR", DisplayName = "Case sensitivity: LogLevel Error from command line.")] + [DataRow("--LogLevel CrItIcal", DisplayName = "Case sensitivity: LogLevel Critical from command line.")] + [DataRow("--LogLevel NONE", DisplayName = "Case sensitivity: LogLevel None from command line.")] + public void TestEngineStartUpWithHighLogLevelOptions(string logLevelOption) + { + _fileSystem!.File.WriteAllText(TEST_RUNTIME_CONFIG_FILE, INITIAL_CONFIG); + + using Process process = ExecuteDabCommand( + command: $"start --config {TEST_RUNTIME_CONFIG_FILE}", + logLevelOption + ); + + // CLI phase messages are at Information level and will be suppressed by Warning+. + // Verify the engine started (process not immediately exited) then clean up. + Assert.IsFalse(process.HasExited); + process.Kill(); + } + + /// + /// Tests that PreParseLogLevel correctly extracts the LogLevel from command line args + /// before full argument parsing, so the CLI logger is configured correctly. + /// + [DataTestMethod] + [DataRow(new string[] { "start", "--LogLevel", "None" }, LogLevel.None, DisplayName = "Parses --LogLevel None")] + [DataRow(new string[] { "start", "--LogLevel", "Warning" }, LogLevel.Warning, DisplayName = "Parses --LogLevel Warning")] + [DataRow(new string[] { "start", "--LogLevel", "Trace" }, LogLevel.Trace, DisplayName = "Parses --LogLevel Trace")] + [DataRow(new string[] { "start", "--LogLevel", "none" }, LogLevel.None, DisplayName = "Case-insensitive: --LogLevel none")] + [DataRow(new string[] { "start", "--LogLevel", "NONE" }, LogLevel.None, DisplayName = "Case-insensitive: --LogLevel NONE")] + [DataRow(new string[] { "start", "--LogLevel", "6" }, LogLevel.None, DisplayName = "Numeric: --LogLevel 6")] + [DataRow(new string[] { "start", "--LogLevel", "0" }, LogLevel.Trace, DisplayName = "Numeric: --LogLevel 0")] + [DataRow(new string[] { "start", "--LogLevel=None" }, LogLevel.None, DisplayName = "Equals syntax: --LogLevel=None")] + [DataRow(new string[] { "start" }, LogLevel.Information, DisplayName = "No --LogLevel returns Information default")] + [DataRow(new string[] { "start", "--verbose" }, LogLevel.Information, DisplayName = "--verbose returns Information default")] + public void TestPreParseLogLevel(string[] args, LogLevel expectedLogLevel) + { + Assert.AreEqual(expected: expectedLogLevel, actual: Program.PreParseLogLevel(args)); + } + /// /// Validates that valid usage of verbs and associated options produce exit code 0 (CliReturnCode.SUCCESS). /// Verifies that explicitly implemented verbs (add, update, init, start) and appropriately diff --git a/src/Cli/CustomLoggerProvider.cs b/src/Cli/CustomLoggerProvider.cs index c06918b93f..e52e2400e3 100644 --- a/src/Cli/CustomLoggerProvider.cs +++ b/src/Cli/CustomLoggerProvider.cs @@ -8,18 +8,30 @@ /// public class CustomLoggerProvider : ILoggerProvider { + private readonly LogLevel _minimumLogLevel; + + public CustomLoggerProvider(LogLevel minimumLogLevel = LogLevel.Information) + { + _minimumLogLevel = minimumLogLevel; + } + public void Dispose() { } /// public ILogger CreateLogger(string categoryName) { - return new CustomConsoleLogger(); + return new CustomConsoleLogger(_minimumLogLevel); } public class CustomConsoleLogger : ILogger { // Minimum LogLevel. LogLevel below this would be disabled. - private readonly LogLevel _minimumLogLevel = LogLevel.Information; + private readonly LogLevel _minimumLogLevel; + + public CustomConsoleLogger(LogLevel minimumLogLevel = LogLevel.Information) + { + _minimumLogLevel = minimumLogLevel; + } // Color values based on LogLevel // LogLevel Foreground Background @@ -61,7 +73,7 @@ public class CustomConsoleLogger : ILogger /// public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) { - if (!IsEnabled(logLevel) || logLevel < _minimumLogLevel) + if (!IsEnabled(logLevel)) { return; } @@ -79,7 +91,7 @@ public void Log(LogLevel logLevel, EventId eventId, TState state, Except /// public bool IsEnabled(LogLevel logLevel) { - return true; + return logLevel != LogLevel.None && logLevel >= _minimumLogLevel; } public IDisposable? BeginScope(TState state) where TState : notnull { diff --git a/src/Cli/Program.cs b/src/Cli/Program.cs index e4732da095..e6732a4871 100644 --- a/src/Cli/Program.cs +++ b/src/Cli/Program.cs @@ -26,6 +26,11 @@ public static int Main(string[] args) // Load environment variables from .env file if present. DotNetEnv.Env.Load(); + // Pre-parse the --LogLevel option so the CLI logger respects the + // requested log level before the engine starts. + LogLevel cliLogLevel = PreParseLogLevel(args); + Utils.LoggerFactoryForCli = Utils.GetLoggerFactoryForCli(cliLogLevel); + // Logger setup and configuration ILoggerFactory loggerFactory = Utils.LoggerFactoryForCli; ILogger cliLogger = loggerFactory.CreateLogger(); @@ -41,6 +46,39 @@ public static int Main(string[] args) return Execute(args, cliLogger, fileSystem, loader); } + /// + /// Pre-parses the --LogLevel option from the command-line arguments before full + /// argument parsing, so the CLI logger can be configured at the right minimum + /// level for CLI phase messages (version info, config loading, etc.). + /// + /// Command line arguments + /// The parsed LogLevel, or Information if not specified or invalid. + internal static LogLevel PreParseLogLevel(string[] args) + { + for (int i = 0; i < args.Length; i++) + { + // Handle --LogLevel None (two separate tokens) + if (args[i].Equals("--LogLevel", StringComparison.OrdinalIgnoreCase) && i + 1 < args.Length) + { + if (Enum.TryParse(args[i + 1], ignoreCase: true, out LogLevel level)) + { + return level; + } + } + // Handle --LogLevel=None (single token with equals sign) + else if (args[i].StartsWith("--LogLevel=", StringComparison.OrdinalIgnoreCase)) + { + string value = args[i]["--LogLevel=".Length..]; + if (Enum.TryParse(value, ignoreCase: true, out LogLevel level)) + { + return level; + } + } + } + + return LogLevel.Information; + } + /// /// Execute the CLI command /// diff --git a/src/Cli/Utils.cs b/src/Cli/Utils.cs index 48edd4411c..a7b19372c2 100644 --- a/src/Cli/Utils.cs +++ b/src/Cli/Utils.cs @@ -960,10 +960,10 @@ public static bool IsEntityProvided(string? entity, ILogger cliLogger, string co /// /// Returns ILoggerFactory with CLI custom logger provider. /// - public static ILoggerFactory GetLoggerFactoryForCli() + public static ILoggerFactory GetLoggerFactoryForCli(LogLevel minimumLogLevel = LogLevel.Information) { ILoggerFactory loggerFactory = new LoggerFactory(); - loggerFactory.AddProvider(new CustomLoggerProvider()); + loggerFactory.AddProvider(new CustomLoggerProvider(minimumLogLevel)); return loggerFactory; } } diff --git a/src/Config/ObjectModel/RuntimeConfig.cs b/src/Config/ObjectModel/RuntimeConfig.cs index 0684040f85..81f796d3dc 100644 --- a/src/Config/ObjectModel/RuntimeConfig.cs +++ b/src/Config/ObjectModel/RuntimeConfig.cs @@ -792,7 +792,8 @@ public LogLevel GetConfiguredLogLevel(string loggerFilter = "") return (LogLevel)value; } - Runtime!.Telemetry!.LoggerLevel!.TryGetValue("default", out value); + value = Runtime!.Telemetry!.LoggerLevel! + .FirstOrDefault(kvp => kvp.Key.Equals("default", StringComparison.OrdinalIgnoreCase)).Value; if (value is not null) { return (LogLevel)value; diff --git a/src/Core/Configurations/RuntimeConfigValidator.cs b/src/Core/Configurations/RuntimeConfigValidator.cs index f5112844da..4802d81efd 100644 --- a/src/Core/Configurations/RuntimeConfigValidator.cs +++ b/src/Core/Configurations/RuntimeConfigValidator.cs @@ -1578,7 +1578,7 @@ private static bool IsLoggerFilterValid(string loggerFilter) { for (int j = 0; j < loggerSub.Length; j++) { - if (!loggerSub[j].Equals(validFiltersSub[j])) + if (!loggerSub[j].Equals(validFiltersSub[j], StringComparison.OrdinalIgnoreCase)) { isValid = false; break; diff --git a/src/Service.Tests/Configuration/ConfigurationTests.cs b/src/Service.Tests/Configuration/ConfigurationTests.cs index aa12a7d465..282949c891 100644 --- a/src/Service.Tests/Configuration/ConfigurationTests.cs +++ b/src/Service.Tests/Configuration/ConfigurationTests.cs @@ -4132,6 +4132,7 @@ public void ValidLogLevelFilters(LogLevel logLevel, Type loggingType) [DataTestMethod] [TestCategory(TestCategory.MSSQL)] [DataRow(LogLevel.Trace, "default")] + [DataRow(LogLevel.Warning, "Default")] [DataRow(LogLevel.Debug, "Azure")] [DataRow(LogLevel.Information, "Azure.DataApiBuilder")] [DataRow(LogLevel.Warning, "Azure.DataApiBuilder.Core")]