Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 57 additions & 14 deletions src/Cli.Tests/EndToEndTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -810,8 +810,9 @@ public Task TestUpdatingStoredProcedureWithRestMethods()

/// <summary>
/// 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.
/// </summary>
/// <param name="logLevelOption">Log level options</param>
[DataTestMethod]
Expand All @@ -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);
Expand All @@ -856,6 +845,60 @@ public void TestEngineStartUpWithVerboseAndLogLevelOptions(string logLevelOption
StringAssert.Contains(output, $"User provided config file: {TEST_RUNTIME_CONFIG_FILE}", StringComparison.Ordinal);
}

/// <summary>
/// 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.
/// </summary>
/// <param name="logLevelOption">Log level options</param>
[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();
}

/// <summary>
/// Tests that PreParseLogLevel correctly extracts the LogLevel from command line args
/// before full argument parsing, so the CLI logger is configured correctly.
/// </summary>
[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));
}

/// <summary>
/// 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
Expand Down
20 changes: 16 additions & 4 deletions src/Cli/CustomLoggerProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,30 @@
/// </summary>
public class CustomLoggerProvider : ILoggerProvider
{
private readonly LogLevel _minimumLogLevel;

public CustomLoggerProvider(LogLevel minimumLogLevel = LogLevel.Information)
{
_minimumLogLevel = minimumLogLevel;
}

public void Dispose() { }

/// <inheritdoc/>
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
Expand Down Expand Up @@ -61,7 +73,7 @@ public class CustomConsoleLogger : ILogger
/// </summary>
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
{
if (!IsEnabled(logLevel) || logLevel < _minimumLogLevel)
if (!IsEnabled(logLevel))
{
return;
}
Expand All @@ -79,7 +91,7 @@ public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Except
/// <inheritdoc/>
public bool IsEnabled(LogLevel logLevel)
{
return true;
return logLevel != LogLevel.None && logLevel >= _minimumLogLevel;
}
public IDisposable? BeginScope<TState>(TState state) where TState : notnull
{
Expand Down
38 changes: 38 additions & 0 deletions src/Cli/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Program> cliLogger = loggerFactory.CreateLogger<Program>();
Expand All @@ -41,6 +46,39 @@ public static int Main(string[] args)
return Execute(args, cliLogger, fileSystem, loader);
}

/// <summary>
/// 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.).
/// </summary>
/// <param name="args">Command line arguments</param>
/// <returns>The parsed LogLevel, or Information if not specified or invalid.</returns>
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;
}

/// <summary>
/// Execute the CLI command
/// </summary>
Expand Down
4 changes: 2 additions & 2 deletions src/Cli/Utils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -960,10 +960,10 @@ public static bool IsEntityProvided(string? entity, ILogger cliLogger, string co
/// <summary>
/// Returns ILoggerFactory with CLI custom logger provider.
/// </summary>
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;
}
}
Expand Down
3 changes: 2 additions & 1 deletion src/Config/ObjectModel/RuntimeConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion src/Core/Configurations/RuntimeConfigValidator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions src/Service.Tests/Configuration/ConfigurationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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")]
Expand Down