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
61 changes: 41 additions & 20 deletions src/Cli.Tests/EndToEndTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -810,35 +810,24 @@ 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
/// Validates that the engine starts successfully when --verbose or --LogLevel is set to a level
/// at or below Information (0-2 / Trace, Debug, Information).
/// CLI phase messages (product name, config path) are emitted at Information level and must
/// therefore appear in stdout.
/// </summary>
/// <param name="logLevelOption">Log level options</param>
/// <param name="logLevelOption">Log level option passed to the start command.</param>
[DataTestMethod]
[DataRow("", DisplayName = "No logging from command line.")]
[DataRow("--verbose", DisplayName = "Verbose logging from command line.")]
[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 @@ -857,6 +846,42 @@ public void TestEngineStartUpWithVerboseAndLogLevelOptions(string logLevelOption
StringAssert.Contains(output, $"User provided config file: {TEST_RUNTIME_CONFIG_FILE}", StringComparison.Ordinal);
}

/// <summary>
/// Validates that the engine starts successfully when --LogLevel is set to Warning or above
/// (3-6 / Warning, Error, Critical, None).
/// CLI phase messages are logged at Information level and are suppressed at Warning+, so no
/// stdout output is expected during the CLI phase.
/// </summary>
/// <param name="logLevelOption">Log level option passed to the start command.</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 are suppressed by Warning+.
// Assert the engine did not exit within a short timeout, then clean up.
bool exitedWithinTimeout = process.WaitForExit(1000);
Assert.IsFalse(exitedWithinTimeout, "Engine process should still be running after 1 second.");
process.Kill();
}

/// <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 Expand Up @@ -1088,10 +1113,6 @@ public async Task TestExitOfRuntimeEngineWithInvalidConfig(
output = await process.StandardOutput.ReadLineAsync();
Assert.IsNotNull(output);
StringAssert.Contains(output, $"Setting default minimum LogLevel:", StringComparison.Ordinal);

output = await process.StandardOutput.ReadLineAsync();
Assert.IsNotNull(output);
StringAssert.Contains(output, "Starting the runtime engine...", StringComparison.Ordinal);
}
else
{
Expand Down
51 changes: 51 additions & 0 deletions src/Cli.Tests/UtilsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -313,3 +313,54 @@ public void TestMergeConfigAvailability(
}
}

/// <summary>
/// Unit tests for <see cref="Cli.Program.PreParseLogLevel"/>.
/// </summary>
[TestClass]
public class ProgramPreParseLogLevelTests
{
/// <summary>
/// Verifies that numeric and named log level values, including mixed-case variants,
/// are parsed correctly from the raw argument array.
/// </summary>
[DataTestMethod]
[DataRow(new string[] { "--LogLevel", "0" }, LogLevel.Trace, DisplayName = "Numeric 0 -> Trace")]
[DataRow(new string[] { "--LogLevel", "1" }, LogLevel.Debug, DisplayName = "Numeric 1 -> Debug")]
[DataRow(new string[] { "--LogLevel", "2" }, LogLevel.Information, DisplayName = "Numeric 2 -> Information")]
[DataRow(new string[] { "--LogLevel", "3" }, LogLevel.Warning, DisplayName = "Numeric 3 -> Warning")]
[DataRow(new string[] { "--LogLevel", "4" }, LogLevel.Error, DisplayName = "Numeric 4 -> Error")]
[DataRow(new string[] { "--LogLevel", "5" }, LogLevel.Critical, DisplayName = "Numeric 5 -> Critical")]
[DataRow(new string[] { "--LogLevel", "6" }, LogLevel.None, DisplayName = "Numeric 6 -> None")]
[DataRow(new string[] { "--LogLevel", "Trace" }, LogLevel.Trace, DisplayName = "Named Trace")]
[DataRow(new string[] { "--LogLevel", "Debug" }, LogLevel.Debug, DisplayName = "Named Debug")]
[DataRow(new string[] { "--LogLevel", "Information" }, LogLevel.Information, DisplayName = "Named Information")]
[DataRow(new string[] { "--LogLevel", "Warning" }, LogLevel.Warning, DisplayName = "Named Warning")]
[DataRow(new string[] { "--LogLevel", "Error" }, LogLevel.Error, DisplayName = "Named Error")]
[DataRow(new string[] { "--LogLevel", "Critical" }, LogLevel.Critical, DisplayName = "Named Critical")]
[DataRow(new string[] { "--LogLevel", "None" }, LogLevel.None, DisplayName = "Named None")]
[DataRow(new string[] { "--LogLevel", "tRace" }, LogLevel.Trace, DisplayName = "Case-insensitive: tRace -> Trace")]
[DataRow(new string[] { "--LogLevel", "NONE" }, LogLevel.None, DisplayName = "Case-insensitive: NONE -> None")]
[DataRow(new string[] { "--LogLevel", "waRNing" }, LogLevel.Warning, DisplayName = "Case-insensitive: waRNing -> Warning")]
public void PreParseLogLevel_ValidValues_ReturnsExpectedLogLevel(string[] args, LogLevel expected)
{
LogLevel actual = Cli.Program.PreParseLogLevel(args);
Assert.AreEqual(expected, actual);
}

/// <summary>
/// Verifies that the default log level (<see cref="LogLevel.Information"/>) is returned
/// when <c>--LogLevel</c> is absent or its value is invalid.
/// </summary>
[DataTestMethod]
[DataRow(new string[] { }, DisplayName = "Empty args -> Information")]
[DataRow(new string[] { "start", "--config", "dab-config.json" }, DisplayName = "No --LogLevel flag -> Information")]
[DataRow(new string[] { "--LogLevel", "bogus" }, DisplayName = "Invalid value -> Information")]
[DataRow(new string[] { "--LogLevel", "7" }, DisplayName = "Out-of-range numeric 7 -> Information")]
[DataRow(new string[] { "--LogLevel" }, DisplayName = "Flag with no value -> Information")]
public void PreParseLogLevel_InvalidOrAbsent_ReturnsInformation(string[] args)
{
LogLevel actual = Cli.Program.PreParseLogLevel(args);
Assert.AreEqual(LogLevel.Information, actual);
}
}

21 changes: 17 additions & 4 deletions src/Cli/CustomLoggerProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,25 @@
/// </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;

// Color values based on LogLevel
// LogLevel Foreground Background
Expand Down Expand Up @@ -56,12 +63,17 @@ public class CustomConsoleLogger : ILogger
{LogLevel.Critical, ConsoleColor.DarkRed}
};

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

/// <summary>
/// Creates Log message by setting console message color based on LogLevel.
/// </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,8 +91,9 @@ 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
{
throw new NotImplementedException();
Expand Down
40 changes: 38 additions & 2 deletions src/Cli/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,12 @@ public static int Main(string[] args)
// Load environment variables from .env file if present.
DotNetEnv.Env.Load();

// Pre-parse --LogLevel from raw args before CommandLine.Parser runs so that
// CLI-phase messages (version info, config path) respect the user-specified level.
LogLevel cliLogLevel = PreParseLogLevel(args);

// Logger setup and configuration
ILoggerFactory loggerFactory = Utils.LoggerFactoryForCli;
ILoggerFactory loggerFactory = Utils.GetLoggerFactoryForCli(cliLogLevel);
ILogger<Program> cliLogger = loggerFactory.CreateLogger<Program>();
ILogger<ConfigGenerator> configGeneratorLogger = loggerFactory.CreateLogger<ConfigGenerator>();
ILogger<Utils> cliUtilsLogger = loggerFactory.CreateLogger<Utils>();
Expand All @@ -37,10 +41,42 @@ public static int Main(string[] args)
// Sets up the filesystem used for reading and writing runtime configuration files.
IFileSystem fileSystem = new FileSystem();
FileSystemRuntimeConfigLoader loader = new(fileSystem, handler: null, isCliLoader: true);

return Execute(args, cliLogger, fileSystem, loader);
}

/// <summary>
/// Scans <paramref name="args"/> for a <c>--LogLevel</c> flag and returns the parsed
/// <see cref="LogLevel"/>. Falls back to <see cref="LogLevel.Information"/> when the
/// flag is absent or its value cannot be parsed.
/// </summary>
/// <param name="args">Raw command-line arguments.</param>
/// <returns>The log level to use for the CLI logging phase.</returns>
public static LogLevel PreParseLogLevel(string[] args)
{
for (int i = 0; i < args.Length - 1; i++)
{
if (string.Equals(args[i], "--LogLevel", StringComparison.OrdinalIgnoreCase))
{
string raw = args[i + 1];
// Accept both integer form (0-6) and named form (Trace, Debug, …).
if (int.TryParse(raw, out int numericLevel)
&& numericLevel >= (int)LogLevel.Trace
&& numericLevel <= (int)LogLevel.None)
{
return (LogLevel)numericLevel;
}

if (Enum.TryParse<LogLevel>(raw, ignoreCase: true, out LogLevel namedLevel)
&& namedLevel >= LogLevel.Trace && namedLevel <= LogLevel.None)
{
return namedLevel;
}
}
}

return LogLevel.Information;
}

/// <summary>
/// Execute the CLI command
/// </summary>
Expand Down
5 changes: 3 additions & 2 deletions src/Cli/Utils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -960,10 +960,11 @@ public static bool IsEntityProvided(string? entity, ILogger cliLogger, string co
/// <summary>
/// Returns ILoggerFactory with CLI custom logger provider.
/// </summary>
public static ILoggerFactory GetLoggerFactoryForCli()
/// <param name="minimumLogLevel">Minimum log level for the CLI logger. Defaults to <see cref="LogLevel.Information"/>.</param>
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
Loading