Skip to content

Commit 6f4b622

Browse files
Hot Reload for Loggers (#2634)
## Why make this change? Closes issues #2580 and #2582 ## What is this change? Adds hot reload feature for loggers which makes it possible for the user to change the log level filters without restarting the application. This is done by ensuring that the logger factories are updated when there is a hot reload event. This feature is also available in production mode, it is the only hot reloadable property in production mode. ## How was this tested? - [ ] Integration Tests - [x] Unit Tests - [X] Manual Testing Test added: - Check if log-level value is properly updated with hot reload, by checking if it is different from previous value and if it is the expected value. ## Sample Request(s) Before hot reload: ![image](https://github.com/user-attachments/assets/70f3c3e7-936c-44df-85ba-a6c1f9b08dd7) ![image](https://github.com/user-attachments/assets/daffabb4-f542-4bdc-8d1b-56a3aa7961c8) After hot reload: ![image](https://github.com/user-attachments/assets/8bc14fce-ef8a-4b4e-99b0-16f81bb0e04e) ![image](https://github.com/user-attachments/assets/f6ba26ea-720c-40de-983b-f33db969d349) --------- Co-authored-by: Ruben Cerna <[email protected]>
1 parent 0f4a3fd commit 6f4b622

File tree

10 files changed

+199
-55
lines changed

10 files changed

+199
-55
lines changed

src/Cli/ConfigGenerator.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1675,7 +1675,7 @@ public static bool TryStartEngineWithOptions(StartOptions options, FileSystemRun
16751675
minimumLogLevel = deserializedRuntimeConfig.GetConfiguredLogLevel();
16761676
HostMode hostModeType = deserializedRuntimeConfig.IsDevelopmentMode() ? HostMode.Development : HostMode.Production;
16771677

1678-
_logger.LogInformation("Setting default minimum LogLevel: {minimumLogLevel} for {hostMode} mode.", minimumLogLevel, hostModeType);
1678+
_logger.LogInformation($"Setting default minimum LogLevel: {minimumLogLevel} for {hostModeType} mode.", minimumLogLevel, hostModeType);
16791679
}
16801680

16811681
args.Add("--LogLevel");

src/Config/DabConfigEvents.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,5 +18,5 @@ public static class DabConfigEvents
1818
public const string GRAPHQL_SCHEMA_REFRESH_ON_CONFIG_CHANGED = "GRAPHQL_SCHEMA_REFRESH_ON_CONFIG_CHANGED";
1919
public const string GRAPHQL_SCHEMA_EVICTION_ON_CONFIG_CHANGED = "GRAPHQL_SCHEMA_EVICTION_ON_CONFIG_CHANGED";
2020
public const string GRAPHQL_SCHEMA_CREATOR_ON_CONFIG_CHANGED = "GRAPHQL_SCHEMA_CREATOR_ON_CONFIG_CHANGED";
21-
21+
public const string LOG_LEVEL_INITIALIZER_ON_CONFIG_CHANGE = "LOG_LEVEL_INITIALIZER_ON_CONFIG_CHANGE";
2222
}

src/Config/FileSystemRuntimeConfigLoader.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ private bool TrySetupConfigFileWatcher()
135135
return false;
136136
}
137137

138-
if (RuntimeConfig is not null && RuntimeConfig.IsDevelopmentMode())
138+
if (RuntimeConfig is not null)
139139
{
140140
try
141141
{
@@ -318,6 +318,8 @@ private void HotReloadConfig(bool isDevMode, ILogger? logger = null)
318318
IsNewConfigDetected = true;
319319
IsNewConfigValidated = false;
320320
SignalConfigChanged();
321+
322+
logger?.LogInformation("Hot-reload process finished.");
321323
}
322324

323325
/// <summary>

src/Config/HotReloadEventHandler.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@ public HotReloadEventHandler()
3333
{ AUTHZ_RESOLVER_ON_CONFIG_CHANGED, null },
3434
{ GRAPHQL_SCHEMA_CREATOR_ON_CONFIG_CHANGED, null },
3535
{ GRAPHQL_SCHEMA_REFRESH_ON_CONFIG_CHANGED, null },
36-
{ GRAPHQL_SCHEMA_EVICTION_ON_CONFIG_CHANGED, null }
36+
{ GRAPHQL_SCHEMA_EVICTION_ON_CONFIG_CHANGED, null },
37+
{ LOG_LEVEL_INITIALIZER_ON_CONFIG_CHANGE, null }
3738
};
3839
}
3940

@@ -49,7 +50,7 @@ public void Subscribe(string eventName, EventHandler<TEventArgs> handler)
4950
{
5051
if (_eventHandlers.ContainsKey(eventName))
5152
{
52-
_eventHandlers[eventName] = handler;
53+
_eventHandlers[eventName] += handler;
5354
}
5455
}
5556
}

src/Config/RuntimeConfigLoader.cs

Lines changed: 48 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -89,22 +89,29 @@ protected void SignalConfigChanged(string message = "")
8989
// Signal that a change has occurred to all change token listeners.
9090
RaiseChanged();
9191

92-
OnConfigChangedEvent(new HotReloadEventArgs(QUERY_MANAGER_FACTORY_ON_CONFIG_CHANGED, message));
93-
OnConfigChangedEvent(new HotReloadEventArgs(METADATA_PROVIDER_FACTORY_ON_CONFIG_CHANGED, message));
94-
OnConfigChangedEvent(new HotReloadEventArgs(QUERY_ENGINE_FACTORY_ON_CONFIG_CHANGED, message));
95-
OnConfigChangedEvent(new HotReloadEventArgs(MUTATION_ENGINE_FACTORY_ON_CONFIG_CHANGED, message));
96-
OnConfigChangedEvent(new HotReloadEventArgs(DOCUMENTOR_ON_CONFIG_CHANGED, message));
97-
98-
// Order of event firing matters: Authorization rules can only be updated after the
99-
// MetadataProviderFactory has been updated with latest database object metadata.
100-
// RuntimeConfig must already be updated and is implied to have been updated by the time
101-
// this function is called.
102-
OnConfigChangedEvent(new HotReloadEventArgs(AUTHZ_RESOLVER_ON_CONFIG_CHANGED, message));
103-
104-
// Order of event firing matters: Eviction must be done before creating a new schema and then updating the schema.
105-
OnConfigChangedEvent(new HotReloadEventArgs(GRAPHQL_SCHEMA_EVICTION_ON_CONFIG_CHANGED, message));
106-
OnConfigChangedEvent(new HotReloadEventArgs(GRAPHQL_SCHEMA_CREATOR_ON_CONFIG_CHANGED, message));
107-
OnConfigChangedEvent(new HotReloadEventArgs(GRAPHQL_SCHEMA_REFRESH_ON_CONFIG_CHANGED, message));
92+
// All the data inside of the if statement should only update when DAB is in development mode.
93+
if (RuntimeConfig!.IsDevelopmentMode())
94+
{
95+
OnConfigChangedEvent(new HotReloadEventArgs(QUERY_MANAGER_FACTORY_ON_CONFIG_CHANGED, message));
96+
OnConfigChangedEvent(new HotReloadEventArgs(METADATA_PROVIDER_FACTORY_ON_CONFIG_CHANGED, message));
97+
OnConfigChangedEvent(new HotReloadEventArgs(QUERY_ENGINE_FACTORY_ON_CONFIG_CHANGED, message));
98+
OnConfigChangedEvent(new HotReloadEventArgs(MUTATION_ENGINE_FACTORY_ON_CONFIG_CHANGED, message));
99+
OnConfigChangedEvent(new HotReloadEventArgs(DOCUMENTOR_ON_CONFIG_CHANGED, message));
100+
101+
// Order of event firing matters: Authorization rules can only be updated after the
102+
// MetadataProviderFactory has been updated with latest database object metadata.
103+
// RuntimeConfig must already be updated and is implied to have been updated by the time
104+
// this function is called.
105+
OnConfigChangedEvent(new HotReloadEventArgs(AUTHZ_RESOLVER_ON_CONFIG_CHANGED, message));
106+
107+
// Order of event firing matters: Eviction must be done before creating a new schema and then updating the schema.
108+
OnConfigChangedEvent(new HotReloadEventArgs(GRAPHQL_SCHEMA_EVICTION_ON_CONFIG_CHANGED, message));
109+
OnConfigChangedEvent(new HotReloadEventArgs(GRAPHQL_SCHEMA_CREATOR_ON_CONFIG_CHANGED, message));
110+
OnConfigChangedEvent(new HotReloadEventArgs(GRAPHQL_SCHEMA_REFRESH_ON_CONFIG_CHANGED, message));
111+
}
112+
113+
// Log Level Initializer is outside of if statement as it can be updated on both development and production mode.
114+
OnConfigChangedEvent(new HotReloadEventArgs(LOG_LEVEL_INITIALIZER_ON_CONFIG_CHANGE, message));
108115
}
109116

110117
/// <summary>
@@ -388,4 +395,29 @@ public void RestoreLkgConfig()
388395
{
389396
RuntimeConfig = LastValidRuntimeConfig;
390397
}
398+
399+
/// <summary>
400+
/// Uses the Last Valid Runtime Config and inserts the log-level property to the Runtime Config that will be used
401+
/// during the hot-reload if DAB is in Production Mode, this means that only changes to log-level will be registered.
402+
/// This is done in order to ensure that no unwanted changes are honored during hot-reload in Production Mode.
403+
/// </summary>
404+
public void InsertWantedChangesInProductionMode()
405+
{
406+
if (!RuntimeConfig!.IsDevelopmentMode())
407+
{
408+
// Creates copy of last valid runtime config and only adds the new logger level changes
409+
RuntimeConfig runtimeConfigCopy = LastValidRuntimeConfig! with
410+
{
411+
Runtime = LastValidRuntimeConfig.Runtime! with
412+
{
413+
Telemetry = LastValidRuntimeConfig.Runtime!.Telemetry! with
414+
{
415+
LoggerLevel = RuntimeConfig.Runtime!.Telemetry!.LoggerLevel
416+
}
417+
}
418+
};
419+
420+
RuntimeConfig = runtimeConfigCopy;
421+
}
422+
}
391423
}

src/Core/Configurations/RuntimeConfigProvider.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,10 @@ public void ValidateConfig()
314314
ILogger<RuntimeConfigValidator> logger = loggerFactory.CreateLogger<RuntimeConfigValidator>();
315315
RuntimeConfigValidator runtimeConfigValidator = new(this, fileSystem, logger, true);
316316

317+
// This function only works if DAB is started in Production Mode
318+
_configLoader.InsertWantedChangesInProductionMode();
319+
320+
// Checks if the new config is valid or invalid
317321
_configLoader.IsNewConfigValidated = runtimeConfigValidator.TryValidateConfig(ConfigFilePath, loggerFactory).Result;
318322

319323
// Saves the lastValidRuntimeConfig as the new RuntimeConfig if it is validated for hot reload

src/Service.Tests/Configuration/HotReload/ConfigurationHotReloadTests.cs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
using Azure.DataApiBuilder.Service.Tests.SqlTests;
1414
using Microsoft.AspNetCore.TestHost;
1515
using Microsoft.Extensions.DependencyInjection;
16+
using Microsoft.Extensions.Logging;
1617
using Microsoft.VisualStudio.TestTools.UnitTesting;
1718

1819
namespace Azure.DataApiBuilder.Service.Tests.Configuration.HotReload;
@@ -49,6 +50,7 @@ private static void GenerateConfigFile(
4950
string restEnabled = "true",
5051
string gQLPath = "/graphQL",
5152
string gQLEnabled = "true",
53+
string logFilter = "debug",
5254
string entityName = "Book",
5355
string sourceObject = "books",
5456
string gQLEntityEnabled = "true",
@@ -91,6 +93,11 @@ private static void GenerateConfigFile(
9193
""provider"": ""StaticWebApps""
9294
},
9395
""mode"": ""development""
96+
},
97+
""telemetry"": {
98+
""log-level"": {
99+
""default"": """ + logFilter + @"""
100+
}
94101
}
95102
},
96103
""entities"": {
@@ -542,6 +549,35 @@ public async Task HotReloadConfigDataSource()
542549
SqlTestHelper.PerformTestEqualJsonStrings(_bookDBOContents, reloadGQLContents.GetProperty("items").ToString());
543550
}
544551

552+
/// <summary>
553+
/// Hot reload the configuration file so that it updated the log-level property.
554+
/// Then we assert that the log-level property is properly updated by ensuring it is
555+
/// not the same as the previous log-level and asserting it is the expected log-level.
556+
/// </summary>
557+
[TestCategory(MSSQL_ENVIRONMENT)]
558+
[TestMethod]
559+
public void HotReloadLogLevel()
560+
{
561+
// Arange
562+
LogLevel expectedLogLevel = LogLevel.Trace;
563+
string expectedFilter = "trace";
564+
RuntimeConfig previousRuntimeConfig = _configProvider.GetConfig();
565+
LogLevel previouslogLevel = previousRuntimeConfig.GetConfiguredLogLevel();
566+
567+
//Act
568+
GenerateConfigFile(
569+
connectionString: $"{ConfigurationTests.GetConnectionStringFromEnvironmentConfig(TestCategory.MSSQL).Replace("\\", "\\\\")}",
570+
logFilter: expectedFilter);
571+
System.Threading.Thread.Sleep(3000);
572+
573+
RuntimeConfig updatedRuntimeConfig = _configProvider.GetConfig();
574+
LogLevel actualLogLevel = updatedRuntimeConfig.GetConfiguredLogLevel();
575+
576+
//Assert
577+
Assert.AreNotEqual(previouslogLevel, actualLogLevel);
578+
Assert.AreEqual(expectedLogLevel, actualLogLevel);
579+
}
580+
545581
/// <summary>
546582
/// Hot reload the configuration file so that it changes from one connection string
547583
/// to an invalid connection string, then it hot reloads once more to the original

src/Service/Program.cs

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
using System.Threading.Tasks;
1010
using Azure.DataApiBuilder.Config;
1111
using Azure.DataApiBuilder.Service.Exceptions;
12+
using Azure.DataApiBuilder.Service.Telemetry;
1213
using Microsoft.ApplicationInsights;
1314
using Microsoft.AspNetCore;
1415
using Microsoft.AspNetCore.Hosting;
@@ -133,17 +134,26 @@ private static ParseResult GetParseResult(Command cmd, string[] args)
133134
/// </summary>
134135
/// <param name="logLevel">minimum log level.</param>
135136
/// <param name="appTelemetryClient">Telemetry client</param>
136-
public static ILoggerFactory GetLoggerFactoryForLogLevel(LogLevel logLevel, TelemetryClient? appTelemetryClient = null)
137+
public static ILoggerFactory GetLoggerFactoryForLogLevel(LogLevel logLevel, TelemetryClient? appTelemetryClient = null, LogLevelInitializer? logLevelInitializer = null)
137138
{
138139
return LoggerFactory
139140
.Create(builder =>
140141
{
141142
// Category defines the namespace we will log from,
142143
// including all sub-domains. ie: "Azure" includes
143144
// "Azure.DataApiBuilder.Service"
144-
builder.AddFilter(category: "Microsoft", logLevel);
145-
builder.AddFilter(category: "Azure", logLevel);
146-
builder.AddFilter(category: "Default", logLevel);
145+
if (logLevelInitializer is null)
146+
{
147+
builder.AddFilter(category: "Microsoft", logLevel);
148+
builder.AddFilter(category: "Azure", logLevel);
149+
builder.AddFilter(category: "Default", logLevel);
150+
}
151+
else
152+
{
153+
builder.AddFilter(category: "Microsoft", level => level >= logLevelInitializer.MinLogLevel);
154+
builder.AddFilter(category: "Azure", level => level >= logLevelInitializer.MinLogLevel);
155+
builder.AddFilter(category: "Default", level => level >= logLevelInitializer.MinLogLevel);
156+
}
147157

148158
// For Sending all the ILogger logs to Application Insights
149159
if (Startup.AppInsightsOptions.Enabled && !string.IsNullOrWhiteSpace(Startup.AppInsightsOptions.ConnectionString))
@@ -157,8 +167,15 @@ public static ILoggerFactory GetLoggerFactoryForLogLevel(LogLevel logLevel, Tele
157167
}
158168
},
159169
configureApplicationInsightsLoggerOptions: (options) => { }
160-
)
161-
.AddFilter<ApplicationInsightsLoggerProvider>(category: string.Empty, logLevel);
170+
);
171+
if (logLevelInitializer is null)
172+
{
173+
builder.AddFilter<ApplicationInsightsLoggerProvider>(category: string.Empty, logLevel);
174+
}
175+
else
176+
{
177+
builder.AddFilter<ApplicationInsightsLoggerProvider>(category: string.Empty, level => level >= logLevelInitializer.MinLogLevel);
178+
}
162179
}
163180

164181
if (Startup.OpenTelemetryOptions.Enabled && !string.IsNullOrWhiteSpace(Startup.OpenTelemetryOptions.Endpoint))

0 commit comments

Comments
 (0)