diff --git a/src/Cli.Tests/EndToEndTests.cs b/src/Cli.Tests/EndToEndTests.cs
index 9648f119c5..530a950d72 100644
--- a/src/Cli.Tests/EndToEndTests.cs
+++ b/src/Cli.Tests/EndToEndTests.cs
@@ -821,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);
@@ -857,6 +845,39 @@ 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
+ );
+
+ string? output = process.StandardOutput.ReadLine();
+ Assert.IsNull(output);
+ process.Kill();
+ }
+
///
/// 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.Tests/UtilsTests.cs b/src/Cli.Tests/UtilsTests.cs
index 3b7a108867..28241e2c85 100644
--- a/src/Cli.Tests/UtilsTests.cs
+++ b/src/Cli.Tests/UtilsTests.cs
@@ -253,9 +253,11 @@ public void TestMergeConfig()
FileSystemRuntimeConfigLoader loader = new(fileSystem);
+ LogBuffer logBuffer = new();
+
Environment.SetEnvironmentVariable(RUNTIME_ENVIRONMENT_VAR_NAME, "Test");
- Assert.IsTrue(ConfigMerger.TryMergeConfigsIfAvailable(fileSystem, loader, new StringLogger(), out string? mergedConfig), "Failed to merge config files");
+ Assert.IsTrue(ConfigMerger.TryMergeConfigsIfAvailable(fileSystem, loader, new StringLogger(), logBuffer, out string? mergedConfig), "Failed to merge config files");
Assert.AreEqual(mergedConfig, "dab-config.Test.merged.json");
Assert.IsTrue(fileSystem.File.Exists(mergedConfig));
Assert.IsTrue(JToken.DeepEquals(JObject.Parse(MERGED_CONFIG), JObject.Parse(fileSystem.File.ReadAllText(mergedConfig))));
@@ -306,10 +308,11 @@ public void TestMergeConfigAvailability(
}
FileSystemRuntimeConfigLoader loader = new(fileSystem);
+ LogBuffer logBuffer = new();
Assert.AreEqual(
expectedIsMergedConfigAvailable,
- ConfigMerger.TryMergeConfigsIfAvailable(fileSystem, loader, new StringLogger(), out string? mergedConfigFile),
+ ConfigMerger.TryMergeConfigsIfAvailable(fileSystem, loader, new StringLogger(), logBuffer, out string? mergedConfigFile),
"Availability of merge config should match");
Assert.AreEqual(expectedMergedConfigFileName, mergedConfigFile, "Merge config file name should match expected");
diff --git a/src/Cli/Commands/StartOptions.cs b/src/Cli/Commands/StartOptions.cs
index 050f410801..2f09fc25ab 100644
--- a/src/Cli/Commands/StartOptions.cs
+++ b/src/Cli/Commands/StartOptions.cs
@@ -19,6 +19,8 @@ public class StartOptions : Options
{
private const string LOGLEVEL_HELPTEXT = "Specifies logging level as provided value. For possible values, see: https://go.microsoft.com/fwlink/?linkid=2263106";
+ public LogBuffer CliBuffer { get; }
+
public StartOptions(bool verbose, LogLevel? logLevel, bool isHttpsRedirectionDisabled, bool mcpStdio, string? mcpRole, string config)
: base(config)
{
@@ -27,6 +29,7 @@ public StartOptions(bool verbose, LogLevel? logLevel, bool isHttpsRedirectionDis
IsHttpsRedirectionDisabled = isHttpsRedirectionDisabled;
McpStdio = mcpStdio;
McpRole = mcpRole;
+ CliBuffer = new LogBuffer();
}
// SetName defines mutually exclusive sets, ie: can not have
@@ -48,11 +51,15 @@ public StartOptions(bool verbose, LogLevel? logLevel, bool isHttpsRedirectionDis
public int Handler(ILogger logger, FileSystemRuntimeConfigLoader loader, IFileSystem fileSystem)
{
- logger.LogInformation("{productName} {version}", PRODUCT_NAME, ProductInfo.GetProductVersion());
+ CliBuffer.BufferLog(Microsoft.Extensions.Logging.LogLevel.Information, $"{PRODUCT_NAME} {ProductInfo.GetProductVersion()}");
bool isSuccess = ConfigGenerator.TryStartEngineWithOptions(this, loader, fileSystem);
if (!isSuccess)
{
+ // Update loggers and flush buffers to ensure that all the logs are printed if the TryStartEngineWithOptions fails.
+ logger = Utils.LoggerFactoryForCli.CreateLogger();
+ CliBuffer.FlushToLogger(logger);
+
logger.LogError("Failed to start the engine{mode}.",
McpStdio ? " in MCP stdio mode" : string.Empty);
}
diff --git a/src/Cli/ConfigGenerator.cs b/src/Cli/ConfigGenerator.cs
index c89da760e4..5a16aff911 100644
--- a/src/Cli/ConfigGenerator.cs
+++ b/src/Cli/ConfigGenerator.cs
@@ -2555,7 +2555,7 @@ public static bool VerifyCanUpdateRelationship(RuntimeConfig runtimeConfig, stri
///
public static bool TryStartEngineWithOptions(StartOptions options, FileSystemRuntimeConfigLoader loader, IFileSystem fileSystem)
{
- if (!TryGetConfigForRuntimeEngine(options.Config, loader, fileSystem, out string runtimeConfigFile))
+ if (!TryGetConfigForRuntimeEngine(options.Config, loader, fileSystem, out string runtimeConfigFile, options.CliBuffer))
{
return false;
}
@@ -2564,17 +2564,17 @@ public static bool TryStartEngineWithOptions(StartOptions options, FileSystemRun
// Replaces all the environment variables while deserializing when starting DAB.
if (!loader.TryLoadKnownConfig(out RuntimeConfig? deserializedRuntimeConfig, replaceEnvVar: true))
{
- _logger.LogError("Failed to parse the config file: {runtimeConfigFile}.", runtimeConfigFile);
+ options.CliBuffer.BufferLog(LogLevel.Error, $"Failed to parse the config file: {runtimeConfigFile}.");
return false;
}
else
{
- _logger.LogInformation("Loaded config file: {runtimeConfigFile}", runtimeConfigFile);
+ options.CliBuffer.BufferLog(LogLevel.Information, $"Loaded config file: {runtimeConfigFile}");
}
if (string.IsNullOrWhiteSpace(deserializedRuntimeConfig.DataSource.ConnectionString))
{
- _logger.LogError("Invalid connection-string provided in the config.");
+ options.CliBuffer.BufferLog(LogLevel.Error, "Invalid connection-string provided in the config.");
return false;
}
@@ -2589,23 +2589,36 @@ public static bool TryStartEngineWithOptions(StartOptions options, FileSystemRun
{
if (options.LogLevel is < LogLevel.Trace or > LogLevel.None)
{
- _logger.LogError(
- "LogLevel's valid range is 0 to 6, your value: {logLevel}, see: https://learn.microsoft.com/dotnet/api/microsoft.extensions.logging.loglevel",
- options.LogLevel);
+ options.CliBuffer.BufferLog(LogLevel.Error,
+ $"LogLevel's valid range is 0 to 6, your value: {options.LogLevel}, see: https://learn.microsoft.com/dotnet/api/microsoft.extensions.logging.loglevel");
return false;
}
minimumLogLevel = (LogLevel)options.LogLevel;
- _logger.LogInformation("Setting minimum LogLevel: {minimumLogLevel}.", minimumLogLevel);
+ options.CliBuffer.BufferLog(LogLevel.Information, $"Setting minimum LogLevel: {minimumLogLevel}.");
}
else
{
minimumLogLevel = deserializedRuntimeConfig.GetConfiguredLogLevel();
HostMode hostModeType = deserializedRuntimeConfig.IsDevelopmentMode() ? HostMode.Development : HostMode.Production;
- _logger.LogInformation($"Setting default minimum LogLevel: {minimumLogLevel} for {hostModeType} mode.", minimumLogLevel, hostModeType);
+ options.CliBuffer.BufferLog(LogLevel.Information, $"Setting default minimum LogLevel: {minimumLogLevel} for {hostModeType} mode.");
}
+ Utils.LoggerFactoryForCli = Utils.GetLoggerFactoryForCli(minimumLogLevel);
+
+ // Update logger for StartOptions
+ ILogger programLogger = Utils.LoggerFactoryForCli.CreateLogger();
+ options.CliBuffer.FlushToLogger(programLogger);
+
+ // Update logger for Utils
+ ILogger utilsLogger = Utils.LoggerFactoryForCli.CreateLogger();
+ Utils.SetCliUtilsLogger(utilsLogger);
+
+ // Update logger for ConfigGenerator
+ ILogger configGeneratorLogger = Utils.LoggerFactoryForCli.CreateLogger();
+ SetLoggerForCliConfigGenerator(configGeneratorLogger);
+
args.Add("--LogLevel");
args.Add(minimumLogLevel.ToString());
@@ -2696,16 +2709,32 @@ public static bool TryGetConfigForRuntimeEngine(
string? configToBeUsed,
FileSystemRuntimeConfigLoader loader,
IFileSystem fileSystem,
- out string runtimeConfigFile)
+ out string runtimeConfigFile,
+ LogBuffer? logBuffer = null)
{
- if (string.IsNullOrEmpty(configToBeUsed) && ConfigMerger.TryMergeConfigsIfAvailable(fileSystem, loader, _logger, out configToBeUsed))
+ if (string.IsNullOrEmpty(configToBeUsed) && ConfigMerger.TryMergeConfigsIfAvailable(fileSystem, loader, _logger, logBuffer, out configToBeUsed))
{
- _logger.LogInformation("Using merged config file based on environment: {configToBeUsed}.", configToBeUsed);
+ if (logBuffer is null)
+ {
+ _logger.LogInformation("Using merged config file based on environment,: {configToBeUsed}.", configToBeUsed);
+ }
+ else
+ {
+ logBuffer.BufferLog(LogLevel.Information, $"Merged config file based on environment is available: {configToBeUsed}.");
+ }
}
- if (!TryGetConfigFileBasedOnCliPrecedence(loader, configToBeUsed, out runtimeConfigFile))
+ if (!TryGetConfigFileBasedOnCliPrecedence(loader, configToBeUsed, out runtimeConfigFile, logBuffer))
{
- _logger.LogError("Config not provided and default config file doesn't exist.");
+ if (logBuffer is null)
+ {
+ _logger.LogError("Config not provided and default config file doesn't exist.");
+ }
+ else
+ {
+ logBuffer.BufferLog(LogLevel.Error, "Config not provided and default config file doesn't exist.");
+ }
+
return false;
}
diff --git a/src/Cli/ConfigMerger.cs b/src/Cli/ConfigMerger.cs
index 55a9ae6ad2..496292df5a 100644
--- a/src/Cli/ConfigMerger.cs
+++ b/src/Cli/ConfigMerger.cs
@@ -15,7 +15,7 @@ public static class ConfigMerger
/// and create a merged file called dab-config.{DAB_ENVIRONMENT}.merged.json
///
/// Returns the name of the merged Config if successful.
- public static bool TryMergeConfigsIfAvailable(IFileSystem fileSystem, FileSystemRuntimeConfigLoader loader, ILogger logger, out string? mergedConfigFile)
+ public static bool TryMergeConfigsIfAvailable(IFileSystem fileSystem, FileSystemRuntimeConfigLoader loader, ILogger logger, LogBuffer? cliBuffer, out string? mergedConfigFile)
{
string? environmentValue = Environment.GetEnvironmentVariable(FileSystemRuntimeConfigLoader.RUNTIME_ENVIRONMENT_VAR_NAME);
mergedConfigFile = null;
@@ -32,16 +32,42 @@ public static bool TryMergeConfigsIfAvailable(IFileSystem fileSystem, FileSystem
string overrideConfigJson = fileSystem.File.ReadAllText(environmentBasedConfigFile);
string currentDir = fileSystem.Directory.GetCurrentDirectory();
- logger.LogInformation("Merging {baseFilePath} and {envFilePath}", Path.Combine(currentDir, baseConfigFile), Path.Combine(currentDir, environmentBasedConfigFile));
+
+ if (cliBuffer is null)
+ {
+ logger.LogInformation("Merging {baseFilePath} and {envFilePath}", Path.Combine(currentDir, baseConfigFile), Path.Combine(currentDir, environmentBasedConfigFile));
+ }
+ else
+ {
+ cliBuffer.BufferLog(LogLevel.Information, $"Merging {Path.Combine(currentDir, baseConfigFile)} and {Path.Combine(currentDir, environmentBasedConfigFile)}");
+ }
+
string mergedConfigJson = MergeJsonProvider.Merge(baseConfigJson, overrideConfigJson);
mergedConfigFile = FileSystemRuntimeConfigLoader.GetMergedFileNameForEnvironment(FileSystemRuntimeConfigLoader.CONFIGFILE_NAME, environmentValue);
fileSystem.File.WriteAllText(mergedConfigFile, mergedConfigJson);
- logger.LogInformation("Generated merged config file: {mergedFile}", Path.Combine(currentDir, mergedConfigFile));
+
+ if (cliBuffer is null)
+ {
+ logger.LogInformation("Generated merged config file: {mergedFile}", Path.Combine(currentDir, mergedConfigFile));
+ }
+ else
+ {
+ cliBuffer.BufferLog(LogLevel.Information, $"Generated merged config file: {Path.Combine(currentDir, mergedConfigFile)}");
+ }
+
return true;
}
catch (Exception ex)
{
- logger.LogError(ex, "Failed to merge the config files.");
+ if (cliBuffer is null)
+ {
+ logger.LogError(ex, "Failed to merge the config files.");
+ }
+ else
+ {
+ cliBuffer.BufferLog(LogLevel.Error, "Failed to merge the config files.", ex);
+ }
+
mergedConfigFile = null;
return false;
}
diff --git a/src/Cli/CustomLoggerProvider.cs b/src/Cli/CustomLoggerProvider.cs
index 0f7a881da8..742d274b68 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
@@ -74,7 +86,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;
}
@@ -97,8 +109,9 @@ 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
{
throw new NotImplementedException();
diff --git a/src/Cli/Exporter.cs b/src/Cli/Exporter.cs
index e694317cd4..1a209e9e27 100644
--- a/src/Cli/Exporter.cs
+++ b/src/Cli/Exporter.cs
@@ -37,7 +37,7 @@ internal class Exporter
public static bool Export(ExportOptions options, ILogger logger, FileSystemRuntimeConfigLoader loader, IFileSystem fileSystem)
{
// Attempt to locate the runtime configuration file based on CLI options
- if (!TryGetConfigFileBasedOnCliPrecedence(loader, options.Config, out string runtimeConfigFile))
+ if (!TryGetConfigFileBasedOnCliPrecedence(loader: loader, userProvidedConfigFile: options.Config, runtimeConfigFile: out string runtimeConfigFile))
{
logger.LogError("Failed to find the config file provided, check your options and try again.");
return false;
diff --git a/src/Cli/Utils.cs b/src/Cli/Utils.cs
index c1ff7f2a99..9e6fc60357 100644
--- a/src/Cli/Utils.cs
+++ b/src/Cli/Utils.cs
@@ -309,19 +309,37 @@ public static bool TryGetRoleAndOperationFromPermission(IEnumerable perm
public static bool TryGetConfigFileBasedOnCliPrecedence(
FileSystemRuntimeConfigLoader loader,
string? userProvidedConfigFile,
- out string runtimeConfigFile)
+ out string runtimeConfigFile,
+ LogBuffer? logBuffer = null)
{
if (!string.IsNullOrEmpty(userProvidedConfigFile))
{
/// The existence of user provided config file is not checked here.
- _logger.LogInformation("User provided config file: {userProvidedConfigFile}", userProvidedConfigFile);
+ if (logBuffer is null)
+ {
+ _logger.LogInformation("User provided config file: {userProvidedConfigFile}", userProvidedConfigFile);
+ }
+ else
+ {
+ logBuffer.BufferLog(LogLevel.Information, $"User provided config file: {userProvidedConfigFile}");
+ }
+
runtimeConfigFile = userProvidedConfigFile;
return true;
}
else
{
- _logger.LogInformation("Config not provided. Trying to get default config based on DAB_ENVIRONMENT...");
- _logger.LogInformation("Environment variable DAB_ENVIRONMENT is {environment}", Environment.GetEnvironmentVariable("DAB_ENVIRONMENT"));
+ if (logBuffer is null)
+ {
+ _logger.LogInformation("Config not provided. Trying to get default config based on DAB_ENVIRONMENT...");
+ _logger.LogInformation("Environment variable DAB_ENVIRONMENT is {environment}", Environment.GetEnvironmentVariable("DAB_ENVIRONMENT"));
+ }
+ else
+ {
+ logBuffer.BufferLog(LogLevel.Information, "Config not provided. Trying to get default config based on DAB_ENVIRONMENT...");
+ logBuffer.BufferLog(LogLevel.Information, $"Environment variable DAB_ENVIRONMENT is {Environment.GetEnvironmentVariable("DAB_ENVIRONMENT")}");
+ }
+
runtimeConfigFile = loader.GetFileNameForEnvironment(null, considerOverrides: false);
}
@@ -961,10 +979,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/FileSystemRuntimeConfigLoader.cs b/src/Config/FileSystemRuntimeConfigLoader.cs
index 7b888a82bf..9be2307229 100644
--- a/src/Config/FileSystemRuntimeConfigLoader.cs
+++ b/src/Config/FileSystemRuntimeConfigLoader.cs
@@ -65,8 +65,6 @@ public class FileSystemRuntimeConfigLoader : RuntimeConfigLoader, IDisposable
///
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_";
@@ -91,8 +89,7 @@ public FileSystemRuntimeConfigLoader(
string baseConfigFilePath = DEFAULT_CONFIG_FILE_NAME,
string? connectionString = null,
bool isCliLoader = false,
- ILogger? logger = null,
- StartupLogBuffer? logBuffer = null)
+ ILogger? logger = null)
: base(handler, connectionString)
{
_fileSystem = fileSystem;
@@ -100,7 +97,6 @@ public FileSystemRuntimeConfigLoader(
ConfigFilePath = GetFinalConfigFilePath();
_isCliLoader = isCliLoader;
_logger = logger;
- _logBuffer = logBuffer;
}
///
@@ -542,10 +538,11 @@ public void SetLogger(ILogger logger)
///
/// Flush all logs from the buffer after the log level is set from the RuntimeConfig.
+ /// Logger needs to be present, or else the logs will be lost.
///
public void FlushLogBuffer()
{
- _logBuffer?.FlushToLogger(_logger);
+ _logBuffer.FlushToLogger(_logger!);
}
///
@@ -558,7 +555,7 @@ private void SendLogToBufferOrLogger(LogLevel logLevel, string message)
{
if (_logger is null)
{
- _logBuffer?.BufferLog(logLevel, message);
+ _logBuffer.BufferLog(logLevel, message);
}
else
{
diff --git a/src/Config/StartupLogBuffer.cs b/src/Config/LogBuffer.cs
similarity index 68%
rename from src/Config/StartupLogBuffer.cs
rename to src/Config/LogBuffer.cs
index 4a01ee7617..f014f012ed 100644
--- a/src/Config/StartupLogBuffer.cs
+++ b/src/Config/LogBuffer.cs
@@ -10,12 +10,12 @@ 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
+ public class LogBuffer
{
- private readonly ConcurrentQueue<(LogLevel LogLevel, string Message)> _logBuffer;
+ private readonly ConcurrentQueue<(LogLevel LogLevel, string Message, Exception? Exception)> _logBuffer;
private readonly object _flushLock = new();
- public StartupLogBuffer()
+ public LogBuffer()
{
_logBuffer = new();
}
@@ -23,21 +23,21 @@ public StartupLogBuffer()
///
/// Buffers a log entry with a specific category name.
///
- public void BufferLog(LogLevel logLevel, string message)
+ public void BufferLog(LogLevel logLevel, string message, Exception? exception = null)
{
- _logBuffer.Enqueue((logLevel, message));
+ _logBuffer.Enqueue((logLevel, message, exception));
}
///
/// Flushes all buffered logs to a single target logger.
///
- public void FlushToLogger(ILogger? targetLogger)
+ public void FlushToLogger(ILogger targetLogger)
{
lock (_flushLock)
{
- while (_logBuffer.TryDequeue(out (LogLevel LogLevel, string Message) entry))
+ while (_logBuffer.TryDequeue(out (LogLevel LogLevel, string Message, Exception? Exception) entry))
{
- targetLogger?.Log(entry.LogLevel, message: entry.Message);
+ targetLogger.Log(entry.LogLevel, message: entry.Message, exception: entry.Exception);
}
}
}
diff --git a/src/Config/RuntimeConfigLoader.cs b/src/Config/RuntimeConfigLoader.cs
index ae5c2dde95..30f9fbe0f9 100644
--- a/src/Config/RuntimeConfigLoader.cs
+++ b/src/Config/RuntimeConfigLoader.cs
@@ -27,6 +27,8 @@ public abstract class RuntimeConfigLoader
private HotReloadEventHandler? _handler;
protected readonly string? _connectionString;
+ protected static LogBuffer _logBuffer = new();
+
// Public to allow the RuntimeProvider and other users of class to set via out param.
// May be candidate to refactor by changing all of the Parse/Load functions to save
// state in place of using out params.
@@ -269,7 +271,7 @@ ex is JsonException ||
// logger can be null when called from CLI
if (logger is null)
{
- Console.Error.WriteLine(errorMessage + $"\n" + $"Message:\n {ex.Message}\n" + $"Stack Trace:\n {ex.StackTrace}");
+ _logBuffer.BufferLog(LogLevel.Error, errorMessage, ex);
}
else
{
diff --git a/src/Service/Program.cs b/src/Service/Program.cs
index d601f4f6b4..04c51d8ab9 100644
--- a/src/Service/Program.cs
+++ b/src/Service/Program.cs
@@ -133,9 +133,9 @@ public static IHostBuilder CreateHostBuilder(string[] args, bool runMcpStdio, st
/// the out param. For log level out of range we throw an exception.
///
/// array that may contain log level information.
- /// sets if log level is found in the args.
+ /// sets if log level is found in the args.
/// Appropriate log level.
- private static LogLevel GetLogLevelFromCommandLineArgs(string[] args, out bool isLogLevelOverridenByCli)
+ private static LogLevel GetLogLevelFromCommandLineArgs(string[] args, out bool isLogLevelOverriddenByCli)
{
Command cmd = new(name: "start");
Option logLevelOption = new(name: "--LogLevel");
@@ -143,7 +143,7 @@ private static LogLevel GetLogLevelFromCommandLineArgs(string[] args, out bool i
ParseResult result = GetParseResult(cmd, args);
bool matchedToken = result.Tokens.Count - result.UnmatchedTokens.Count - result.UnparsedTokens.Count > 1;
LogLevel logLevel = matchedToken ? result.GetValueForOption(logLevelOption) : LogLevel.Error;
- isLogLevelOverridenByCli = matchedToken;
+ isLogLevelOverriddenByCli = matchedToken;
if (logLevel is > LogLevel.None or < LogLevel.Trace)
{
diff --git a/src/Service/Startup.cs b/src/Service/Startup.cs
index 618feef454..5070e3adfa 100644
--- a/src/Service/Startup.cs
+++ b/src/Service/Startup.cs
@@ -86,10 +86,10 @@ 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;
+ private LogBuffer _logBuffer = new();
public IConfiguration Configuration { get; } = configuration;
@@ -106,7 +106,6 @@ public class Startup(IConfiguration configuration, ILogger logger)
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;
@@ -114,7 +113,7 @@ public void ConfigureServices(IServiceCollection services)
FileSystemRuntimeConfigLoader.RUNTIME_ENV_CONNECTION_STRING.Replace(FileSystemRuntimeConfigLoader.ENVIRONMENT_PREFIX, ""),
null);
IFileSystem fileSystem = new FileSystem();
- FileSystemRuntimeConfigLoader configLoader = new(fileSystem, _hotReloadEventHandler, configFileName, connectionString, logBuffer: _logBuffer);
+ FileSystemRuntimeConfigLoader configLoader = new(fileSystem, _hotReloadEventHandler, configFileName, connectionString);
RuntimeConfigProvider configProvider = new(configLoader);
_configProvider = configProvider;
@@ -160,32 +159,32 @@ public void ConfigureServices(IServiceCollection services)
.WithMetrics(metrics =>
{
metrics.SetResourceBuilder(ResourceBuilder.CreateDefault().AddService(runtimeConfig.Runtime.Telemetry.OpenTelemetry.ServiceName!))
- // TODO: should we also add FusionCache metrics?
- // To do so we just need to add the package ZiggyCreatures.FusionCache.OpenTelemetry and call
- // .AddFusionCacheInstrumentation()
- .AddOtlpExporter(configure =>
- {
- configure.Endpoint = otlpEndpoint;
- configure.Headers = runtimeConfig.Runtime.Telemetry.OpenTelemetry.Headers;
- configure.Protocol = OtlpExportProtocol.Grpc;
- })
- .AddMeter(TelemetryMetricsHelper.MeterName);
+ // TODO: should we also add FusionCache metrics?
+ // To do so we just need to add the package ZiggyCreatures.FusionCache.OpenTelemetry and call
+ // .AddFusionCacheInstrumentation()
+ .AddOtlpExporter(configure =>
+ {
+ configure.Endpoint = otlpEndpoint;
+ configure.Headers = runtimeConfig.Runtime.Telemetry.OpenTelemetry.Headers;
+ configure.Protocol = OtlpExportProtocol.Grpc;
+ })
+ .AddMeter(TelemetryMetricsHelper.MeterName);
})
.WithTracing(tracing =>
{
tracing.SetResourceBuilder(ResourceBuilder.CreateDefault().AddService(runtimeConfig.Runtime.Telemetry.OpenTelemetry.ServiceName!))
- .AddHttpClientInstrumentation()
- // TODO: should we also add FusionCache traces?
- // To do so we just need to add the package ZiggyCreatures.FusionCache.OpenTelemetry and call
- // .AddFusionCacheInstrumentation()
- .AddHotChocolateInstrumentation()
- .AddOtlpExporter(configure =>
- {
- configure.Endpoint = otlpEndpoint;
- configure.Headers = runtimeConfig.Runtime.Telemetry.OpenTelemetry.Headers;
- configure.Protocol = OtlpExportProtocol.Grpc;
- })
- .AddSource(TelemetryTracesHelper.DABActivitySource.Name);
+ .AddHttpClientInstrumentation()
+ // TODO: should we also add FusionCache traces?
+ // To do so we just need to add the package ZiggyCreatures.FusionCache.OpenTelemetry and call
+ // .AddFusionCacheInstrumentation()
+ .AddHotChocolateInstrumentation()
+ .AddOtlpExporter(configure =>
+ {
+ configure.Endpoint = otlpEndpoint;
+ configure.Headers = runtimeConfig.Runtime.Telemetry.OpenTelemetry.Headers;
+ configure.Protocol = OtlpExportProtocol.Grpc;
+ })
+ .AddSource(TelemetryTracesHelper.DABActivitySource.Name);
});
}
@@ -600,7 +599,7 @@ private void ConfigureResponseCompression(IServiceCollection services, RuntimeCo
options.Level = systemCompressionLevel;
});
- _logger.LogDebug("Response compression enabled with level '{compressionLevel}' for REST, GraphQL, and MCP endpoints.", compressionLevel);
+ _logBuffer.BufferLog(LogLevel.Information, $"Response compression enabled with level '{compressionLevel}' for REST, GraphQL, and MCP endpoints.");
}
///
@@ -649,42 +648,42 @@ private void AddGraphQLService(IServiceCollection services, GraphQLRuntimeOption
{
if (error.Exception is not null)
{
- _logger.LogError(exception: error.Exception, message: "A GraphQL request execution error occurred.");
+ _logger.LogError(error.Exception, "A GraphQL request execution error occurred.");
return error.WithMessage(error.Exception.Message);
}
if (error.Code is not null)
{
- _logger.LogError(message: "Error code: {errorCode}\nError message: {errorMessage}", error.Code, error.Message);
+ _logger.LogError($"Error code: {error.Code}\nError message: {error.Message}");
return error.WithMessage(error.Message);
}
return error;
})
- .AddErrorFilter(error =>
+ .AddErrorFilter(error =>
+ {
+ if (error.Exception is DataApiBuilderException thrownException)
{
- if (error.Exception is DataApiBuilderException thrownException)
- {
- error = error
- .WithException(null)
- .WithMessage(thrownException.Message)
- .WithCode($"{thrownException.SubStatusCode}");
+ error = error
+ .WithException(null)
+ .WithMessage(thrownException.Message)
+ .WithCode($"{thrownException.SubStatusCode}");
- // If user error i.e. validation error or conflict error with datasource, then retain location/path
- if (!thrownException.StatusCode.IsClientError())
- {
- error = error.WithLocations(Array.Empty());
- }
+ // If user error i.e. validation error or conflict error with datasource, then retain location/path
+ if (!thrownException.StatusCode.IsClientError())
+ {
+ error = error.WithLocations(Array.Empty());
}
+ }
- return error;
- })
- // Allows DAB to override the HTTP error code set by HotChocolate.
- // This is used to ensure HTTP code 4XX is set when the datatbase
- // returns a "bad request" error such as stored procedure params missing.
- .UseRequest()
- .UseRequest()
- .UseDefaultPipeline();
+ return error;
+ })
+ // Allows DAB to override the HTTP error code set by HotChocolate.
+ // This is used to ensure HTTP code 4XX is set when the datatbase
+ // returns a "bad request" error such as stored procedure params missing.
+ .UseRequest()
+ .UseRequest()
+ .UseDefaultPipeline();
}
///
@@ -702,44 +701,72 @@ private void RefreshGraphQLSchema(IServiceCollection services)
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, RuntimeConfigProvider runtimeConfigProvider, IHostApplicationLifetime hostLifetime)
{
bool isRuntimeReady = false;
+ RuntimeConfig? runtimeConfig = null;
- if (runtimeConfigProvider.TryGetConfig(out RuntimeConfig? runtimeConfig))
+ try
{
- // Set LogLevel based on RuntimeConfig
- DynamicLogLevelProvider logLevelProvider = app.ApplicationServices.GetRequiredService();
- logLevelProvider.UpdateFromRuntimeConfig(runtimeConfig);
- FileSystemRuntimeConfigLoader configLoader = app.ApplicationServices.GetRequiredService();
+ if (runtimeConfigProvider.TryGetConfig(out runtimeConfig))
+ {
+ // Create log level initializer for Startup, which allows it to respond to runtime config changes and update the log level accordingly.
+ LogLevelInitializer logLevelInit = new(MinimumLogLevel, typeof(Startup).FullName, runtimeConfigProvider, _hotReloadEventHandler);
+
+ // Set LogLevel based on RuntimeConfig
+ DynamicLogLevelProvider logLevelProvider = app.ApplicationServices.GetRequiredService();
+ logLevelProvider.UpdateFromRuntimeConfig(runtimeConfig);
- //Flush all logs that were buffered before setting the LogLevel
- configLoader.SetLogger(app.ApplicationServices.GetRequiredService>());
- configLoader.FlushLogBuffer();
+ // Configure Telemetry
+ // TODO: Issue #3239. Refactor this methods so that they are all called before creating the new logger factory.
+ ConfigureApplicationInsightsTelemetry(app, runtimeConfig, logLevelInit);
+ ConfigureOpenTelemetry(app, runtimeConfig, logLevelInit);
+ ConfigureAzureLogAnalytics(app, runtimeConfig, logLevelInit);
+ ConfigureFileSink(app, runtimeConfig, logLevelInit);
- // Configure Telemetry
- ConfigureApplicationInsightsTelemetry(app, runtimeConfig);
- ConfigureOpenTelemetry(runtimeConfig);
- ConfigureAzureLogAnalytics(runtimeConfig);
- ConfigureFileSink(app, runtimeConfig);
+ // Flush all logs that were buffered before setting the LogLevel.
+ // Important: All logs set before this point should use _logBuffer.
+ FlushAllLogs(app);
- // Config provided before starting the engine.
- isRuntimeReady = PerformOnConfigChangeAsync(app).Result;
+ // Config provided before starting the engine.
+ isRuntimeReady = PerformOnConfigChangeAsync(app).Result;
- if (!isRuntimeReady)
+ if (!isRuntimeReady)
+ {
+ _logger.LogError(
+ message: "Could not initialize the engine with the runtime config file: {configFilePath}",
+ runtimeConfigProvider.ConfigFilePath);
+ hostLifetime.StopApplication();
+ }
+ }
+ else
{
- _logger.LogError(
- message: "Could not initialize the engine with the runtime config file: {configFilePath}",
- runtimeConfigProvider.ConfigFilePath);
- hostLifetime.StopApplication();
+ // Config provided during runtime.
+ runtimeConfigProvider.IsLateConfigured = true;
+ runtimeConfigProvider.RuntimeConfigLoadedHandlers.Add(async (_, _) =>
+ {
+ // This section will only run if the runtime config is provided during runtime. E.g. IsLateConfigured is true.
+ // Set LogLevel based on RuntimeConfig
+ RuntimeConfigProvider runtimeConfigProvider = app.ApplicationServices.GetService()!;
+ RuntimeConfig runtimeConfig = runtimeConfigProvider.GetConfig();
+ DynamicLogLevelProvider logLevelProvider = app.ApplicationServices.GetRequiredService();
+ logLevelProvider.UpdateFromRuntimeConfig(runtimeConfig);
+
+ //Flush all logs that were buffered before setting the LogLevel.
+ // Important: All logs set before this point should use _logBuffer.
+ FlushAllLogs(app);
+
+ isRuntimeReady = await PerformOnConfigChangeAsync(app);
+
+ return isRuntimeReady;
+ });
}
}
- else
+ finally
{
- // Config provided during runtime.
- runtimeConfigProvider.IsLateConfigured = true;
- runtimeConfigProvider.RuntimeConfigLoadedHandlers.Add(async (_, _) =>
+ // Attempt one final flush in case there was any exception that caused the
+ // previous section to throw an error before flushing the logs.
+ if (!runtimeConfigProvider.IsLateConfigured)
{
- isRuntimeReady = await PerformOnConfigChangeAsync(app);
- return isRuntimeReady;
- });
+ FlushAllLogs(app);
+ }
}
if (env.IsDevelopment())
@@ -945,7 +972,7 @@ private void ConfigureAuthentication(IServiceCollection services, RuntimeConfigP
if (easyAuthType == EasyAuthType.AppService && !appServiceEnvironmentDetected)
{
- _logger.LogWarning(AppServiceAuthenticationInfo.APPSERVICE_DEV_MISSING_ENV_CONFIG);
+ _logBuffer.BufferLog(LogLevel.Warning, AppServiceAuthenticationInfo.APPSERVICE_DEV_MISSING_ENV_CONFIG);
}
string defaultScheme = easyAuthType == EasyAuthType.AppService
@@ -955,7 +982,7 @@ private void ConfigureAuthentication(IServiceCollection services, RuntimeConfigP
services.AddAuthentication(defaultScheme)
.AddEnvDetectedEasyAuth();
- _logger.LogInformation("Registered EasyAuth scheme: {Scheme}", defaultScheme);
+ _logBuffer.BufferLog(LogLevel.Information, $"Registered EasyAuth scheme: {defaultScheme}");
}
else if (authOptions.IsUnauthenticatedAuthenticationProvider())
@@ -1015,7 +1042,7 @@ private static void ConfigureAuthenticationV2(IServiceCollection services, Runti
/// The application builder.
/// The provider used to load runtime configuration.
///
- private void ConfigureApplicationInsightsTelemetry(IApplicationBuilder app, RuntimeConfig runtimeConfig)
+ private void ConfigureApplicationInsightsTelemetry(IApplicationBuilder app, RuntimeConfig runtimeConfig, LogLevelInitializer logLevelInit)
{
if (runtimeConfig?.Runtime?.Telemetry is not null
&& runtimeConfig.Runtime.Telemetry.ApplicationInsights is not null)
@@ -1024,13 +1051,13 @@ private void ConfigureApplicationInsightsTelemetry(IApplicationBuilder app, Runt
if (!AppInsightsOptions.Enabled)
{
- _logger.LogInformation("Application Insights are disabled.");
+ _logBuffer.BufferLog(LogLevel.Information, "Application Insights are disabled.");
return;
}
if (string.IsNullOrWhiteSpace(AppInsightsOptions.ConnectionString))
{
- _logger.LogWarning("Logs won't be sent to Application Insights because an Application Insights connection string is not available in the runtime config.");
+ _logBuffer.BufferLog(LogLevel.Warning, "Logs won't be sent to Application Insights because an Application Insights connection string is not available in the runtime config.");
return;
}
@@ -1038,7 +1065,7 @@ private void ConfigureApplicationInsightsTelemetry(IApplicationBuilder app, Runt
if (appTelemetryClient is null)
{
- _logger.LogError("Telemetry client is not initialized.");
+ _logBuffer.BufferLog(LogLevel.Error, "Telemetry client is not initialized.");
return;
}
@@ -1053,7 +1080,7 @@ private void ConfigureApplicationInsightsTelemetry(IApplicationBuilder app, Runt
}
// Updating Startup Logger to Log from Startup Class.
- ILoggerFactory loggerFactory = Program.GetLoggerFactoryForLogLevel(MinimumLogLevel, appTelemetryClient);
+ ILoggerFactory? loggerFactory = CreateLoggerFactoryForHostedAndNonHostedScenario(app.ApplicationServices, logLevelInit);
_logger = loggerFactory.CreateLogger();
}
}
@@ -1063,7 +1090,7 @@ private void ConfigureApplicationInsightsTelemetry(IApplicationBuilder app, Runt
/// is enabled, we can track different events and metrics.
///
/// The provider used to load runtime configuration.
- private void ConfigureOpenTelemetry(RuntimeConfig runtimeConfig)
+ private void ConfigureOpenTelemetry(IApplicationBuilder app, RuntimeConfig runtimeConfig, LogLevelInitializer logLevelInit)
{
if (runtimeConfig?.Runtime?.Telemetry is not null
&& runtimeConfig.Runtime.Telemetry.OpenTelemetry is not null)
@@ -1072,19 +1099,19 @@ private void ConfigureOpenTelemetry(RuntimeConfig runtimeConfig)
if (!OpenTelemetryOptions.Enabled)
{
- _logger.LogInformation("Open Telemetry is disabled.");
+ _logBuffer.BufferLog(LogLevel.Information, "Open Telemetry is disabled.");
return;
}
if (string.IsNullOrWhiteSpace(OpenTelemetryOptions?.Endpoint)
|| !Uri.TryCreate(OpenTelemetryOptions.Endpoint, UriKind.Absolute, out _))
{
- _logger.LogWarning("Logs won't be sent to Open Telemetry because a valid Open Telemetry endpoint URI is not available in the runtime config.");
+ _logBuffer.BufferLog(LogLevel.Warning, "Logs won't be sent to Open Telemetry because a valid Open Telemetry endpoint URI is not available in the runtime config.");
return;
}
// Updating Startup Logger to Log from Startup Class.
- ILoggerFactory? loggerFactory = Program.GetLoggerFactoryForLogLevel(MinimumLogLevel);
+ ILoggerFactory? loggerFactory = CreateLoggerFactoryForHostedAndNonHostedScenario(app.ApplicationServices, logLevelInit);
_logger = loggerFactory.CreateLogger();
}
}
@@ -1094,7 +1121,7 @@ private void ConfigureOpenTelemetry(RuntimeConfig runtimeConfig)
/// is enabled, we can track different events and metrics.
///
/// The provider used to load runtime configuration.
- private void ConfigureAzureLogAnalytics(RuntimeConfig runtimeConfig)
+ private void ConfigureAzureLogAnalytics(IApplicationBuilder app, RuntimeConfig runtimeConfig, LogLevelInitializer logLevelInit)
{
if (runtimeConfig?.Runtime?.Telemetry is not null
&& runtimeConfig.Runtime.Telemetry.AzureLogAnalytics is not null)
@@ -1103,26 +1130,26 @@ private void ConfigureAzureLogAnalytics(RuntimeConfig runtimeConfig)
if (!AzureLogAnalyticsOptions.Enabled)
{
- _logger.LogInformation("Azure Log Analytics is disabled.");
+ _logBuffer.BufferLog(LogLevel.Information, "Azure Log Analytics is disabled.");
return;
}
bool isAuthIncomplete = false;
if (string.IsNullOrEmpty(AzureLogAnalyticsOptions.Auth?.CustomTableName))
{
- _logger.LogError("Logs won't be sent to Azure Log Analytics because the Custom Table Name is not available in the config file.");
+ _logBuffer.BufferLog(LogLevel.Error, "Logs won't be sent to Azure Log Analytics because the Custom Table Name is not available in the config file.");
isAuthIncomplete = true;
}
if (string.IsNullOrEmpty(AzureLogAnalyticsOptions.Auth?.DcrImmutableId))
{
- _logger.LogError("Logs won't be sent to Azure Log Analytics because the DCR Immutable Id is not available in the config file.");
+ _logBuffer.BufferLog(LogLevel.Error, "Logs won't be sent to Azure Log Analytics because the DCR Immutable Id is not available in the config file.");
isAuthIncomplete = true;
}
if (string.IsNullOrEmpty(AzureLogAnalyticsOptions.Auth?.DceEndpoint))
{
- _logger.LogError("Logs won't be sent to Azure Log Analytics because the DCE Endpoint is not available in the config file.");
+ _logBuffer.BufferLog(LogLevel.Error, "Logs won't be sent to Azure Log Analytics because the DCE Endpoint is not available in the config file.");
isAuthIncomplete = true;
}
@@ -1132,7 +1159,7 @@ private void ConfigureAzureLogAnalytics(RuntimeConfig runtimeConfig)
}
// Updating Startup Logger to Log from Startup Class.
- ILoggerFactory? loggerFactory = Program.GetLoggerFactoryForLogLevel(MinimumLogLevel);
+ ILoggerFactory? loggerFactory = CreateLoggerFactoryForHostedAndNonHostedScenario(app.ApplicationServices, logLevelInit);
_logger = loggerFactory.CreateLogger();
}
}
@@ -1143,7 +1170,7 @@ private void ConfigureAzureLogAnalytics(RuntimeConfig runtimeConfig)
///
/// The application builder.
/// The provider used to load runtime configuration.
- private void ConfigureFileSink(IApplicationBuilder app, RuntimeConfig runtimeConfig)
+ private void ConfigureFileSink(IApplicationBuilder app, RuntimeConfig runtimeConfig, LogLevelInitializer logLevelInit)
{
if (runtimeConfig?.Runtime?.Telemetry is not null
&& runtimeConfig.Runtime.Telemetry.File is not null)
@@ -1152,25 +1179,25 @@ private void ConfigureFileSink(IApplicationBuilder app, RuntimeConfig runtimeCon
if (!FileSinkOptions.Enabled)
{
- _logger.LogInformation("File is disabled.");
+ _logBuffer.BufferLog(LogLevel.Information, "File is disabled.");
return;
}
if (string.IsNullOrWhiteSpace(FileSinkOptions.Path))
{
- _logger.LogError("Logs won't be sent to File because the Path is not available in the config file.");
+ _logBuffer.BufferLog(LogLevel.Error, "Logs won't be sent to File because the Path is not available in the config file.");
return;
}
Logger? serilogLogger = app.ApplicationServices.GetService();
if (serilogLogger is null)
{
- _logger.LogError("Serilog Logger Configuration is not set.");
+ _logBuffer.BufferLog(LogLevel.Error, "Serilog Logger Configuration is not set.");
return;
}
// Updating Startup Logger to Log from Startup Class.
- ILoggerFactory? loggerFactory = Program.GetLoggerFactoryForLogLevel(logLevel: MinimumLogLevel, serilogLogger: serilogLogger);
+ ILoggerFactory? loggerFactory = CreateLoggerFactoryForHostedAndNonHostedScenario(app.ApplicationServices, logLevelInit);
_logger = loggerFactory.CreateLogger();
}
}
@@ -1338,6 +1365,7 @@ private bool IsOboConfigured()
///
public static void AddValidFilters()
{
+ LoggerFilters.AddFilter(typeof(Startup).FullName);
LoggerFilters.AddFilter(typeof(RuntimeConfigValidator).FullName);
LoggerFilters.AddFilter(typeof(SqlQueryEngine).FullName);
LoggerFilters.AddFilter(typeof(IQueryExecutor).FullName);
@@ -1363,5 +1391,18 @@ public static bool IsAzureLogAnalyticsAvailable(AzureLogAnalyticsOptions azureLo
&& !string.IsNullOrWhiteSpace(azureLogAnalyticsOptions.Auth.DcrImmutableId)
&& !string.IsNullOrWhiteSpace(azureLogAnalyticsOptions.Auth.DceEndpoint);
}
+
+ ///
+ /// Helper function that sets the logger for FileSystemRuntimeConfigLoader and flushes the buffered logs to the logger.
+ ///
+ /// Contains all the services needed to set the logger.
+ private void FlushAllLogs(IApplicationBuilder app)
+ {
+ FileSystemRuntimeConfigLoader configLoader = app.ApplicationServices.GetRequiredService();
+ configLoader.SetLogger(app.ApplicationServices.GetRequiredService>());
+
+ _logBuffer.FlushToLogger(_logger);
+ configLoader.FlushLogBuffer();
+ }
}
}
diff --git a/src/Service/Telemetry/DynamicLogLevelProvider.cs b/src/Service/Telemetry/DynamicLogLevelProvider.cs
index 3c35e295e6..36f1cd2e42 100644
--- a/src/Service/Telemetry/DynamicLogLevelProvider.cs
+++ b/src/Service/Telemetry/DynamicLogLevelProvider.cs
@@ -8,21 +8,43 @@ public class DynamicLogLevelProvider
public LogLevel CurrentLogLevel { get; private set; }
public bool IsCliOverridden { get; private set; }
+ ///
+ /// Sets the initial log level, which can be passed from the CLI or default to Error if in Production mode or Debug if in Development mode.
+ /// Also sets whether the log level was overridden by the CLI, which will prevent updates from runtime config changes.
+ ///
+ /// The initial log level to set.
+ /// Indicates whether the log level was overridden by the CLI.
public void SetInitialLogLevel(LogLevel logLevel = LogLevel.Error, bool isCliOverridden = false)
{
CurrentLogLevel = logLevel;
IsCliOverridden = isCliOverridden;
}
- public void UpdateFromRuntimeConfig(RuntimeConfig runtimeConfig)
+ ///
+ /// Updates the current log level based on the runtime configuration, unless it was overridden by the CLI.
+ ///
+ /// The runtime configuration to use for updating the log level.
+ /// Optional logger filter to apply when determining the log level.
+ public void UpdateFromRuntimeConfig(RuntimeConfig runtimeConfig, string? loggerFilter = null)
{
// Only update if CLI didn't override
if (!IsCliOverridden)
{
- CurrentLogLevel = runtimeConfig.GetConfiguredLogLevel();
+ if (loggerFilter is null)
+ {
+ loggerFilter = string.Empty;
+ }
+
+ CurrentLogLevel = runtimeConfig.GetConfiguredLogLevel(loggerFilter);
}
}
+ ///
+ /// Used to dynamically determine whether a log should be emitted based on the current log level.
+ /// This allows for dynamic log level changes at runtime without needing to restart the application.
+ ///
+ /// The log level of the log that wants to be emitted.
+ /// True if the log should be emitted, false otherwise.
public bool ShouldLog(LogLevel logLevel)
{
return logLevel >= CurrentLogLevel;