From d4ad8a3c913063ce63fedb8ac47fa1e3f6848bc8 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 7 Mar 2026 00:57:02 +0000
Subject: [PATCH 1/7] Initial plan
From 36bfd9a3357eb414ea27429abd8b3ad43cc9b806 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 7 Mar 2026 01:17:43 +0000
Subject: [PATCH 2/7] Fix LogLevel incorrect behavior: CLI phase messages and
case-insensitive Default key
Co-authored-by: RubenCerna2079 <32799214+RubenCerna2079@users.noreply.github.com>
---
src/Cli.Tests/EndToEndTests.cs | 71 +++++++++++++++----
src/Cli/CustomLoggerProvider.cs | 20 ++++--
src/Cli/Program.cs | 38 ++++++++++
src/Cli/Utils.cs | 4 +-
src/Config/ObjectModel/RuntimeConfig.cs | 3 +-
.../Configurations/RuntimeConfigValidator.cs | 2 +-
.../Configuration/ConfigurationTests.cs | 1 +
7 files changed, 117 insertions(+), 22 deletions(-)
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")]
From 6188dc7ee3d48d11ccdb25a3fe17d70883ab8cf4 Mon Sep 17 00:00:00 2001
From: Ruben Cerna
Date: Wed, 11 Mar 2026 17:06:38 -0700
Subject: [PATCH 3/7] Fix log level logic
---
src/Cli.Tests/EndToEndTests.cs | 24 -------
src/Cli/Program.cs | 40 +-----------
src/Config/FileSystemRuntimeConfigLoader.cs | 70 +++++++++++++++++----
src/Config/ObjectModel/RuntimeConfig.cs | 1 -
src/Service/Program.cs | 18 ++++--
src/Service/Startup.cs | 19 +++++-
6 files changed, 89 insertions(+), 83 deletions(-)
diff --git a/src/Cli.Tests/EndToEndTests.cs b/src/Cli.Tests/EndToEndTests.cs
index 5b56a1d3c1..ae4499c5ec 100644
--- a/src/Cli.Tests/EndToEndTests.cs
+++ b/src/Cli.Tests/EndToEndTests.cs
@@ -879,26 +879,6 @@ public void TestEngineStartUpWithHighLogLevelOptions(string logLevelOption)
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
@@ -1130,10 +1110,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
{
diff --git a/src/Cli/Program.cs b/src/Cli/Program.cs
index e6732a4871..86f0351a90 100644
--- a/src/Cli/Program.cs
+++ b/src/Cli/Program.cs
@@ -26,11 +26,6 @@ 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();
@@ -42,43 +37,10 @@ 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);
}
- ///
- /// 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/Config/FileSystemRuntimeConfigLoader.cs b/src/Config/FileSystemRuntimeConfigLoader.cs
index 614cfbd11c..8aebf12634 100644
--- a/src/Config/FileSystemRuntimeConfigLoader.cs
+++ b/src/Config/FileSystemRuntimeConfigLoader.cs
@@ -58,6 +58,13 @@ public class FileSystemRuntimeConfigLoader : RuntimeConfigLoader
///
private readonly IFileSystem _fileSystem;
+ ///
+ /// Logger used to log all the events that occur inside of FileSystemRuntimeConfigLoader
+ ///
+ private ILogger? _logger;
+
+ private StartupLogBuffer? _logBuffer;
+
public const string CONFIGFILE_NAME = "dab-config";
public const string CONFIG_EXTENSION = ".json";
public const string ENVIRONMENT_PREFIX = "DAB_";
@@ -81,13 +88,17 @@ public FileSystemRuntimeConfigLoader(
HotReloadEventHandler? handler = null,
string baseConfigFilePath = DEFAULT_CONFIG_FILE_NAME,
string? connectionString = null,
- bool isCliLoader = false)
+ bool isCliLoader = false,
+ ILogger? logger = null,
+ StartupLogBuffer? logBuffer = null)
: base(handler, connectionString)
{
_fileSystem = fileSystem;
_baseConfigFilePath = baseConfigFilePath;
ConfigFilePath = GetFinalConfigFilePath();
_isCliLoader = isCliLoader;
+ _logger = logger;
+ _logBuffer = logBuffer;
}
///
@@ -195,7 +206,14 @@ public bool TryLoadConfig(
{
if (_fileSystem.File.Exists(path))
{
- Console.WriteLine($"Loading config file from {_fileSystem.Path.GetFullPath(path)}.");
+ if (_logger is null)
+ {
+ _logBuffer?.BufferLog(LogLevel.Information, $"Loading config file from {_fileSystem.Path.GetFullPath(path)}.");
+ }
+ else
+ {
+ _logger?.LogInformation($"Loading config file from {_fileSystem.Path.GetFullPath(path)}.");
+ }
// Use File.ReadAllText because DAB doesn't need write access to the file
// and ensures the file handle is released immediately after reading.
@@ -214,7 +232,15 @@ public bool TryLoadConfig(
}
catch (IOException ex)
{
- Console.WriteLine($"IO Exception, retrying due to {ex.Message}");
+ if (_logger is null)
+ {
+ _logBuffer?.BufferLog(LogLevel.Warning, $"IO Exception, retrying due to {ex.Message}");
+ }
+ else
+ {
+ _logger?.LogWarning($"IO Exception, retrying due to {ex.Message}");
+ }
+
if (runCount == FileUtilities.RunLimit)
{
throw;
@@ -237,8 +263,14 @@ public bool TryLoadConfig(
{
if (TrySetupConfigFileWatcher())
{
- Console.WriteLine("Monitoring config: {0} for hot-reloading.", ConfigFilePath);
- logger?.LogInformation("Monitoring config: {ConfigFilePath} for hot-reloading.", ConfigFilePath);
+ if (_logger is null)
+ {
+ _logBuffer?.BufferLog(LogLevel.Information, $"Monitoring config: {ConfigFilePath} for hot-reloading.");
+ }
+ else
+ {
+ _logger?.LogInformation($"Monitoring config: {ConfigFilePath} for hot-reloading.");
+ }
}
// When isDevMode is not null it means we are in a hot-reload scenario, and need to save the previous
@@ -248,13 +280,13 @@ public bool TryLoadConfig(
// Log error when the mode is changed during hot-reload.
if (isDevMode != this.RuntimeConfig.IsDevelopmentMode())
{
- if (logger is null)
+ if (_logger is null)
{
- Console.WriteLine("Hot-reload doesn't support switching mode. Please restart the service to switch the mode.");
+ _logBuffer?.BufferLog(LogLevel.Error, "Hot-reload doesn't support switching mode. Please restart the service to switch the mode.");
}
else
{
- logger.LogError("Hot-reload doesn't support switching mode. Please restart the service to switch the mode.");
+ _logger?.LogError("Hot-reload doesn't support switching mode. Please restart the service to switch the mode.");
}
}
@@ -280,15 +312,14 @@ public bool TryLoadConfig(
return false;
}
- if (logger is null)
+ string errorMessage = $"Unable to find config file: {path} does not exist.";
+ if (_logger is null)
{
- string errorMessage = $"Unable to find config file: {path} does not exist.";
- Console.Error.WriteLine(errorMessage);
+ _logBuffer?.BufferLog(LogLevel.Error, errorMessage);
}
else
{
- string errorMessage = "Unable to find config file: {path} does not exist.";
- logger.LogError(message: errorMessage, path);
+ _logger?.LogError(message: errorMessage);
}
config = null;
@@ -515,4 +546,17 @@ public void UpdateConfigFilePath(string filePath)
_baseConfigFilePath = filePath;
ConfigFilePath = filePath;
}
+
+ public void SetLogger(ILogger logger)
+ {
+ _logger = logger;
+ }
+
+ ///
+ /// Flush all logs from the buffer after the log level is set from the RuntimeConfig.
+ ///
+ public void FlushLogBuffer()
+ {
+ _logBuffer?.FlushToLogger(_logger);
+ }
}
diff --git a/src/Config/ObjectModel/RuntimeConfig.cs b/src/Config/ObjectModel/RuntimeConfig.cs
index 81f796d3dc..0e81e55e51 100644
--- a/src/Config/ObjectModel/RuntimeConfig.cs
+++ b/src/Config/ObjectModel/RuntimeConfig.cs
@@ -771,7 +771,6 @@ Runtime.Telemetry.LoggerLevel is null ||
///
public LogLevel GetConfiguredLogLevel(string loggerFilter = "")
{
-
if (!IsLogLevelNull())
{
int max = 0;
diff --git a/src/Service/Program.cs b/src/Service/Program.cs
index e23fb98cd9..368eaffc46 100644
--- a/src/Service/Program.cs
+++ b/src/Service/Program.cs
@@ -33,6 +33,7 @@ namespace Azure.DataApiBuilder.Service
public class Program
{
public static bool IsHttpsRedirectionDisabled { get; private set; }
+ public static DynamicLogLevelProvider LogLevelProvider = new();
public static void Main(string[] args)
{
@@ -59,7 +60,6 @@ public static void Main(string[] args)
public static bool StartEngine(string[] args, bool runMcpStdio, string? mcpRole)
{
- Console.WriteLine("Starting the runtime engine...");
try
{
IHost host = CreateHostBuilder(args, runMcpStdio, mcpRole).Build();
@@ -107,9 +107,19 @@ public static IHostBuilder CreateHostBuilder(string[] args, bool runMcpStdio, st
McpStdioHelper.ConfigureMcpStdio(builder, mcpRole);
}
})
+ .ConfigureServices((context, services) =>
+ {
+ services.AddSingleton(LogLevelProvider);
+ })
+ .ConfigureLogging(logging =>
+ {
+ logging.AddFilter("Microsoft", logLevel => LogLevelProvider.ShouldLog(logLevel));
+ logging.AddFilter("Microsoft.Hosting.Lifetime", logLevel => LogLevelProvider.ShouldLog(logLevel));
+ })
.ConfigureWebHostDefaults(webBuilder =>
{
Startup.MinimumLogLevel = GetLogLevelFromCommandLineArgs(args, out Startup.IsLogLevelOverriddenByCli);
+ LogLevelProvider.SetInitialLogLevel(Startup.MinimumLogLevel, Startup.IsLogLevelOverriddenByCli);
ILoggerFactory loggerFactory = GetLoggerFactoryForLogLevel(Startup.MinimumLogLevel, stdio: runMcpStdio);
ILogger startupLogger = loggerFactory.CreateLogger();
DisableHttpsRedirectionIfNeeded(args);
@@ -185,9 +195,9 @@ public static ILoggerFactory GetLoggerFactoryForLogLevel(
// "Azure.DataApiBuilder.Service"
if (logLevelInitializer is null)
{
- builder.AddFilter(category: "Microsoft", logLevel);
- builder.AddFilter(category: "Azure", logLevel);
- builder.AddFilter(category: "Default", logLevel);
+ builder.AddFilter(category: "Microsoft", logLevel => LogLevelProvider.ShouldLog(logLevel));
+ builder.AddFilter(category: "Azure", logLevel => LogLevelProvider.ShouldLog(logLevel));
+ builder.AddFilter(category: "Default", logLevel => LogLevelProvider.ShouldLog(logLevel));
}
else
{
diff --git a/src/Service/Startup.cs b/src/Service/Startup.cs
index f9e7043440..340cf6e1ef 100644
--- a/src/Service/Startup.cs
+++ b/src/Service/Startup.cs
@@ -85,6 +85,7 @@ public class Startup(IConfiguration configuration, ILogger logger)
public static AzureLogAnalyticsOptions AzureLogAnalyticsOptions = new();
public static FileSinkOptions FileSinkOptions = new();
public const string NO_HTTPS_REDIRECT_FLAG = "--no-https-redirect";
+ private StartupLogBuffer _logBuffer = new();
private readonly HotReloadEventHandler _hotReloadEventHandler = new();
private RuntimeConfigProvider? _configProvider;
private ILogger _logger = logger;
@@ -104,13 +105,14 @@ public class Startup(IConfiguration configuration, ILogger logger)
public void ConfigureServices(IServiceCollection services)
{
Startup.AddValidFilters();
+ services.AddSingleton(_logBuffer);
services.AddSingleton(_hotReloadEventHandler);
string configFileName = Configuration.GetValue("ConfigFileName") ?? FileSystemRuntimeConfigLoader.DEFAULT_CONFIG_FILE_NAME;
string? connectionString = Configuration.GetValue(
FileSystemRuntimeConfigLoader.RUNTIME_ENV_CONNECTION_STRING.Replace(FileSystemRuntimeConfigLoader.ENVIRONMENT_PREFIX, ""),
null);
IFileSystem fileSystem = new FileSystem();
- FileSystemRuntimeConfigLoader configLoader = new(fileSystem, _hotReloadEventHandler, configFileName, connectionString);
+ FileSystemRuntimeConfigLoader configLoader = new(fileSystem, _hotReloadEventHandler, configFileName, connectionString, logBuffer: _logBuffer);
RuntimeConfigProvider configProvider = new(configLoader);
_configProvider = configProvider;
@@ -229,6 +231,13 @@ public void ConfigureServices(IServiceCollection services)
services.AddHealthChecks()
.AddCheck(nameof(BasicHealthCheck));
+ services.AddSingleton>(implementationFactory: (serviceProvider) =>
+ {
+ LogLevelInitializer logLevelInit = new(MinimumLogLevel, typeof(FileSystemRuntimeConfigLoader).FullName, _configProvider, _hotReloadEventHandler);
+ ILoggerFactory? loggerFactory = CreateLoggerFactoryForHostedAndNonHostedScenario(serviceProvider, logLevelInit);
+ return loggerFactory.CreateLogger();
+ });
+
services.AddSingleton>(implementationFactory: (serviceProvider) =>
{
LogLevelInitializer logLevelInit = new(MinimumLogLevel, typeof(SqlQueryEngine).FullName, _configProvider, _hotReloadEventHandler);
@@ -693,7 +702,13 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env, RuntimeC
if (runtimeConfigProvider.TryGetConfig(out RuntimeConfig? runtimeConfig))
{
- // Configure Application Insights Telemetry
+ // Configure Telemetry
+ DynamicLogLevelProvider logLevelProvider = app.ApplicationServices.GetRequiredService();
+ logLevelProvider.UpdateFromRuntimeConfig(runtimeConfig);
+ FileSystemRuntimeConfigLoader configLoader = app.ApplicationServices.GetRequiredService();
+ configLoader.SetLogger(app.ApplicationServices.GetRequiredService>());
+ configLoader.FlushLogBuffer();
+
ConfigureApplicationInsightsTelemetry(app, runtimeConfig);
ConfigureOpenTelemetry(runtimeConfig);
ConfigureAzureLogAnalytics(runtimeConfig);
From ad13f1fd2209dc1da43f4b71c801fb369e4fb5f0 Mon Sep 17 00:00:00 2001
From: Ruben Cerna
Date: Wed, 11 Mar 2026 17:23:56 -0700
Subject: [PATCH 4/7] Remove unnecessary code
---
src/Cli/CustomLoggerProvider.cs | 20 ++++----------------
src/Cli/Program.cs | 3 +--
src/Cli/Utils.cs | 4 ++--
3 files changed, 7 insertions(+), 20 deletions(-)
diff --git a/src/Cli/CustomLoggerProvider.cs b/src/Cli/CustomLoggerProvider.cs
index e52e2400e3..c06918b93f 100644
--- a/src/Cli/CustomLoggerProvider.cs
+++ b/src/Cli/CustomLoggerProvider.cs
@@ -8,30 +8,18 @@
///
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(_minimumLogLevel);
+ return new CustomConsoleLogger();
}
public class CustomConsoleLogger : ILogger
{
// Minimum LogLevel. LogLevel below this would be disabled.
- private readonly LogLevel _minimumLogLevel;
-
- public CustomConsoleLogger(LogLevel minimumLogLevel = LogLevel.Information)
- {
- _minimumLogLevel = minimumLogLevel;
- }
+ private readonly LogLevel _minimumLogLevel = LogLevel.Information;
// Color values based on LogLevel
// LogLevel Foreground Background
@@ -73,7 +61,7 @@ public CustomConsoleLogger(LogLevel minimumLogLevel = LogLevel.Information)
///
public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter)
{
- if (!IsEnabled(logLevel))
+ if (!IsEnabled(logLevel) || logLevel < _minimumLogLevel)
{
return;
}
@@ -91,7 +79,7 @@ public void Log(LogLevel logLevel, EventId eventId, TState state, Except
///
public bool IsEnabled(LogLevel logLevel)
{
- return logLevel != LogLevel.None && logLevel >= _minimumLogLevel;
+ return true;
}
public IDisposable? BeginScope(TState state) where TState : notnull
{
diff --git a/src/Cli/Program.cs b/src/Cli/Program.cs
index 86f0351a90..a7720c407c 100644
--- a/src/Cli/Program.cs
+++ b/src/Cli/Program.cs
@@ -36,8 +36,7 @@ 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);
-
+ FileSystemRuntimeConfigLoader loader = new(fileSystem, handler: null, isCliLoader: true);
return Execute(args, cliLogger, fileSystem, loader);
}
diff --git a/src/Cli/Utils.cs b/src/Cli/Utils.cs
index a7b19372c2..48edd4411c 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(LogLevel minimumLogLevel = LogLevel.Information)
+ public static ILoggerFactory GetLoggerFactoryForCli()
{
ILoggerFactory loggerFactory = new LoggerFactory();
- loggerFactory.AddProvider(new CustomLoggerProvider(minimumLogLevel));
+ loggerFactory.AddProvider(new CustomLoggerProvider());
return loggerFactory;
}
}
From fe81966223ffcb732cf71fa0326f5053b92a62c1 Mon Sep 17 00:00:00 2001
From: Ruben Cerna
Date: Wed, 11 Mar 2026 17:31:56 -0700
Subject: [PATCH 5/7] Add missing new classes
---
src/Config/StartupLogBuffer.cs | 45 +++++++++++++++++++
.../Telemetry/DynamicLogLevelProvider.cs | 31 +++++++++++++
2 files changed, 76 insertions(+)
create mode 100644 src/Config/StartupLogBuffer.cs
create mode 100644 src/Service/Telemetry/DynamicLogLevelProvider.cs
diff --git a/src/Config/StartupLogBuffer.cs b/src/Config/StartupLogBuffer.cs
new file mode 100644
index 0000000000..01ac5a3b3b
--- /dev/null
+++ b/src/Config/StartupLogBuffer.cs
@@ -0,0 +1,45 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using Microsoft.Extensions.Logging;
+using System.Collections.Concurrent;
+
+namespace Azure.DataApiBuilder.Config
+{
+ ///
+ /// A general-purpose log buffer that stores log entries before the final log level is determined.
+ /// Can be used across different components during startup to capture important early logs.
+ ///
+ public class StartupLogBuffer
+ {
+ private readonly ConcurrentQueue<(LogLevel LogLevel, string Message)> _logBuffer;
+ private readonly object _flushLock = new();
+
+ public StartupLogBuffer()
+ {
+ _logBuffer = new();
+ }
+
+ ///
+ /// Buffers a log entry with a specific category name.
+ ///
+ public void BufferLog(LogLevel logLevel, string message)
+ {
+ _logBuffer.Enqueue((logLevel, message));
+ }
+
+ ///
+ /// Flushes all buffered logs to a single target logger.
+ ///
+ public void FlushToLogger(ILogger? targetLogger)
+ {
+ lock (_flushLock)
+ {
+ while (_logBuffer.TryDequeue(out (LogLevel LogLevel, string Message) entry))
+ {
+ targetLogger?.Log(entry.LogLevel, message: entry.Message);
+ }
+ }
+ }
+ }
+}
diff --git a/src/Service/Telemetry/DynamicLogLevelProvider.cs b/src/Service/Telemetry/DynamicLogLevelProvider.cs
new file mode 100644
index 0000000000..3c35e295e6
--- /dev/null
+++ b/src/Service/Telemetry/DynamicLogLevelProvider.cs
@@ -0,0 +1,31 @@
+using Azure.DataApiBuilder.Config.ObjectModel;
+using Microsoft.Extensions.Logging;
+
+namespace Azure.DataApiBuilder.Service.Telemetry
+{
+ public class DynamicLogLevelProvider
+ {
+ public LogLevel CurrentLogLevel { get; private set; }
+ public bool IsCliOverridden { get; private set; }
+
+ public void SetInitialLogLevel(LogLevel logLevel = LogLevel.Error, bool isCliOverridden = false)
+ {
+ CurrentLogLevel = logLevel;
+ IsCliOverridden = isCliOverridden;
+ }
+
+ public void UpdateFromRuntimeConfig(RuntimeConfig runtimeConfig)
+ {
+ // Only update if CLI didn't override
+ if (!IsCliOverridden)
+ {
+ CurrentLogLevel = runtimeConfig.GetConfiguredLogLevel();
+ }
+ }
+
+ public bool ShouldLog(LogLevel logLevel)
+ {
+ return logLevel >= CurrentLogLevel;
+ }
+ }
+}
From 9e0c115f299fa7f651b713eec040bf09421a17c5 Mon Sep 17 00:00:00 2001
From: Ruben Cerna
Date: Wed, 11 Mar 2026 20:40:30 -0700
Subject: [PATCH 6/7] Add missing service
---
src/Service/Startup.cs | 1 +
1 file changed, 1 insertion(+)
diff --git a/src/Service/Startup.cs b/src/Service/Startup.cs
index ac5e638d1f..dea13dad6b 100644
--- a/src/Service/Startup.cs
+++ b/src/Service/Startup.cs
@@ -106,6 +106,7 @@ public void ConfigureServices(IServiceCollection services)
{
Startup.AddValidFilters();
services.AddSingleton(_logBuffer);
+ services.AddSingleton(Program.LogLevelProvider);
services.AddSingleton(_hotReloadEventHandler);
string configFileName = Configuration.GetValue("ConfigFileName") ?? FileSystemRuntimeConfigLoader.DEFAULT_CONFIG_FILE_NAME;
string? connectionString = Configuration.GetValue(
From 1f4ee8d7e7daa1d70702768ca70141b071822141 Mon Sep 17 00:00:00 2001
From: Ruben Cerna
Date: Wed, 11 Mar 2026 20:46:35 -0700
Subject: [PATCH 7/7] Fix syntax
---
src/Cli/Program.cs | 2 +-
src/Config/StartupLogBuffer.cs | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/Cli/Program.cs b/src/Cli/Program.cs
index 127934c2de..6a079a469c 100644
--- a/src/Cli/Program.cs
+++ b/src/Cli/Program.cs
@@ -36,7 +36,7 @@ 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);
+ FileSystemRuntimeConfigLoader loader = new(fileSystem, handler: null, isCliLoader: true);
return Execute(args, cliLogger, fileSystem, loader);
}
diff --git a/src/Config/StartupLogBuffer.cs b/src/Config/StartupLogBuffer.cs
index 01ac5a3b3b..4a01ee7617 100644
--- a/src/Config/StartupLogBuffer.cs
+++ b/src/Config/StartupLogBuffer.cs
@@ -1,8 +1,8 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
-using Microsoft.Extensions.Logging;
using System.Collections.Concurrent;
+using Microsoft.Extensions.Logging;
namespace Azure.DataApiBuilder.Config
{