Skip to content

Commit f1f2019

Browse files
RubenCerna2079Ruben Cerna
and
Ruben Cerna
authored
Enable Custom log-level for Classes via log-level Filters (#2620)
## Why make this change? This change solves tasks, #2584, #2585, #2586, #2587, #2588, #2569. All of them are part of the bigger task of #2563 which adds filters for logging. ## What is this change? This change allows the users to add more specific logging filters to their config file, this means that log-levels meant for a specific class will take priority over the default log-level. In the following example, even if the default is in `debug`, the logging for `IQueryExecutor` will be in `information`. ``` "Azure.DataApiBuilder.Core.Resolvers.IQueryExecutor": "information", "default": "debug" ``` Specific Changes: - Changed the schema file so that it allows for different keywords that represent the classes, allowing each of them to have a different log-level. It also allows the user to add a general log-level through the use of the keyword `default`  - Updated DAB Validate so that it checks if the keywords used in the config file are valid classes that we use to give logging information to the users. - Change logic that deserializes the new properties in the config file and stores them inside of the `TelemetryOptions` class and it uses the saved log-levels to dictate if a specific logger uses a different log-level from the default. This change gets rid of the `loggerFactory` and changes it to a dictionary. ## How was this tested? - [ ] Integration Tests - [X] Unit Tests Following tests were added: - Test that filters are deserialized correctly and their values are what we expect them to be. - Test that invalid filters throw an error. - Test that between two filters, the one that is more specific takes priority. ## Sample Request(s) ![image](https://github.com/user-attachments/assets/4dbf7a98-7aba-4781-97f3-4b32fa8d77e0) --------- Co-authored-by: Ruben Cerna <[email protected]>
1 parent 97c25db commit f1f2019

File tree

12 files changed

+346
-112
lines changed

12 files changed

+346
-112
lines changed

Diff for: schemas/dab.draft.schema.json

+4-4
Original file line numberDiff line numberDiff line change
@@ -367,11 +367,11 @@
367367
},
368368
"log-level": {
369369
"type": "object",
370-
"description": "Global configuration of log level",
370+
"description": "Global configuration of log level, defines logging severity levels for specific classes, when 'null' it will set logging level based on 'host: mode' property",
371371
"additionalProperties": false,
372-
"properties": {
373-
"level": {
374-
"description": "Defines logging severity levels, when in default value it will set logging level based on 'host: mode' property",
372+
"patternProperties": {
373+
"^[a-zA-Z_]\\w{0,}(\\.\\w{1,}){0,}$": {
374+
"description": "Only specific namespaces that are used for logging in DAB are able to be used here. The regex pattern checks that it only allows for namespace values. So it only accepts a letter as its first value, from there it accepts any alphanumeric character, each section is divided by '.' and there cannot be more than 1 '.' in a row",
375375
"type": "string",
376376
"default": null,
377377
"enum": [

Diff for: src/Cli/Commands/ValidateOptions.cs

+2
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System.IO.Abstractions;
55
using Azure.DataApiBuilder.Config;
66
using Azure.DataApiBuilder.Product;
7+
using Azure.DataApiBuilder.Service;
78
using Cli.Constants;
89
using CommandLine;
910
using Microsoft.Extensions.Logging;
@@ -28,6 +29,7 @@ public ValidateOptions(string config)
2829
public int Handler(ILogger logger, FileSystemRuntimeConfigLoader loader, IFileSystem fileSystem)
2930
{
3031
logger.LogInformation("{productName} {version}", PRODUCT_NAME, ProductInfo.GetProductVersion());
32+
Startup.AddValidFilters();
3133
bool isValidConfig = ConfigGenerator.IsConfigValid(this, loader, fileSystem);
3234

3335
if (isValidConfig)

Diff for: src/Cli/ConfigGenerator.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -1672,7 +1672,7 @@ public static bool TryStartEngineWithOptions(StartOptions options, FileSystemRun
16721672
}
16731673
else
16741674
{
1675-
minimumLogLevel = RuntimeConfig.GetConfiguredLogLevel(deserializedRuntimeConfig);
1675+
minimumLogLevel = deserializedRuntimeConfig.GetConfiguredLogLevel();
16761676
HostMode hostModeType = deserializedRuntimeConfig.IsDevelopmentMode() ? HostMode.Development : HostMode.Production;
16771677

16781678
_logger.LogInformation("Setting default minimum LogLevel: {minimumLogLevel} for {hostMode} mode.", minimumLogLevel, hostModeType);

Diff for: src/Config/Converters/LogLevelOptionsConverterFactory.cs

-49
This file was deleted.

Diff for: src/Config/ObjectModel/LogLevelOptions.cs

-21
This file was deleted.

Diff for: src/Config/ObjectModel/RuntimeConfig.cs

+47-10
Original file line numberDiff line numberDiff line change
@@ -578,11 +578,26 @@ public uint GetPaginationLimit(int? first)
578578
/// <summary>
579579
/// Checks if the property log-level or its value are null
580580
/// </summary>
581-
public bool IsLogLevelNull() =>
582-
Runtime is null ||
583-
Runtime.Telemetry is null ||
584-
Runtime.Telemetry.LoggerLevel is null ||
585-
Runtime.Telemetry.LoggerLevel.Value is null;
581+
public bool IsLogLevelNull()
582+
{
583+
if (Runtime is null ||
584+
Runtime.Telemetry is null ||
585+
Runtime.Telemetry.LoggerLevel is null ||
586+
Runtime.Telemetry.LoggerLevel.Count == 0)
587+
{
588+
return true;
589+
}
590+
591+
foreach (KeyValuePair<string, LogLevel?> logger in Runtime!.Telemetry.LoggerLevel)
592+
{
593+
if (logger.Key == null)
594+
{
595+
return true;
596+
}
597+
}
598+
599+
return false;
600+
}
586601

587602
/// <summary>
588603
/// Takes in the RuntimeConfig object and checks the LogLevel.
@@ -591,15 +606,37 @@ Runtime.Telemetry.LoggerLevel is null ||
591606
/// If host mode is Development, return `LogLevel.Debug`, else
592607
/// for production returns `LogLevel.Error`.
593608
/// </summary>
594-
public static LogLevel GetConfiguredLogLevel(RuntimeConfig runtimeConfig)
609+
public LogLevel GetConfiguredLogLevel(string loggerFilter = "")
595610
{
596-
LogLevel? value = runtimeConfig.Runtime?.Telemetry?.LoggerLevel?.Value;
597-
if (value is not null)
611+
612+
if (!IsLogLevelNull())
598613
{
599-
return (LogLevel)value;
614+
int max = 0;
615+
string currentFilter = string.Empty;
616+
foreach (KeyValuePair<string, LogLevel?> logger in Runtime!.Telemetry!.LoggerLevel!)
617+
{
618+
// Checks if the new key that is valid has more priority than the current key
619+
if (logger.Key.Length > max && loggerFilter.StartsWith(logger.Key))
620+
{
621+
max = logger.Key.Length;
622+
currentFilter = logger.Key;
623+
}
624+
}
625+
626+
Runtime!.Telemetry!.LoggerLevel!.TryGetValue(currentFilter, out LogLevel? value);
627+
if (value is not null)
628+
{
629+
return (LogLevel)value;
630+
}
631+
632+
Runtime!.Telemetry!.LoggerLevel!.TryGetValue("default", out value);
633+
if (value is not null)
634+
{
635+
return (LogLevel)value;
636+
}
600637
}
601638

602-
if (runtimeConfig.IsDevelopmentMode())
639+
if (IsDevelopmentMode())
603640
{
604641
return LogLevel.Debug;
605642
}

Diff for: src/Config/ObjectModel/TelemetryOptions.cs

+3-2
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,15 @@
22
// Licensed under the MIT License.
33

44
using System.Text.Json.Serialization;
5+
using Microsoft.Extensions.Logging;
56

67
namespace Azure.DataApiBuilder.Config.ObjectModel;
78

89
/// <summary>
910
/// Represents the options for telemetry.
1011
/// </summary>
11-
public record TelemetryOptions(ApplicationInsightsOptions? ApplicationInsights = null, OpenTelemetryOptions? OpenTelemetry = null, LogLevelOptions? LoggerLevel = null)
12+
public record TelemetryOptions(ApplicationInsightsOptions? ApplicationInsights = null, OpenTelemetryOptions? OpenTelemetry = null, Dictionary<string, LogLevel?>? LoggerLevel = null)
1213
{
1314
[JsonPropertyName("log-level")]
14-
public LogLevelOptions? LoggerLevel { get; init; } = LoggerLevel;
15+
public Dictionary<string, LogLevel?>? LoggerLevel { get; init; } = LoggerLevel;
1516
}

Diff for: src/Config/RuntimeConfigLoader.cs

-1
Original file line numberDiff line numberDiff line change
@@ -248,7 +248,6 @@ public static JsonSerializerOptions GetSerializationOptions(
248248
options.Converters.Add(new MultipleMutationOptionsConverter(options));
249249
options.Converters.Add(new DataSourceConverterFactory(replaceEnvVar));
250250
options.Converters.Add(new HostOptionsConvertorFactory());
251-
options.Converters.Add(new LogLevelOptionsConverterFactory());
252251

253252
if (replaceEnvVar)
254253
{

Diff for: src/Core/Configurations/LoggerFilters.cs

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
namespace Azure.DataApiBuilder.Core.Configurations
5+
{
6+
public static class LoggerFilters
7+
{
8+
public static List<string> validFilters = new();
9+
10+
public static void AddFilter(string? loggerFilter)
11+
{
12+
if (loggerFilter != null)
13+
{
14+
validFilters.Add(loggerFilter);
15+
}
16+
}
17+
}
18+
}

Diff for: src/Core/Configurations/RuntimeConfigValidator.cs

+56
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ public void ValidateConfigProperties()
7373
ValidateAuthenticationOptions(runtimeConfig);
7474
ValidateGlobalEndpointRouteConfig(runtimeConfig);
7575
ValidateAppInsightsTelemetryConnectionString(runtimeConfig);
76+
ValidateLoggerFilters(runtimeConfig);
7677

7778
// Running these graphQL validations only in development mode to ensure
7879
// fast startup of engine in production mode.
@@ -129,6 +130,25 @@ public void ValidateAppInsightsTelemetryConnectionString(RuntimeConfig runtimeCo
129130
}
130131
}
131132

133+
/// <summary>
134+
/// Only certain classes can have different log levels, this function ensures that only those classes are used in the config file.
135+
/// </summary>
136+
public static void ValidateLoggerFilters(RuntimeConfig runtimeConfig)
137+
{
138+
if (runtimeConfig.Runtime?.Telemetry is not null && runtimeConfig.Runtime.Telemetry.LoggerLevel is not null)
139+
{
140+
Dictionary<string, LogLevel?> loggerLevelOptions = runtimeConfig.Runtime.Telemetry.LoggerLevel;
141+
142+
foreach (KeyValuePair<string, LogLevel?> logger in loggerLevelOptions)
143+
{
144+
if (!IsLoggerFilterValid(logger.Key))
145+
{
146+
throw new NotSupportedException($"Log level filter {logger.Key} needs to be of a valid log class.");
147+
}
148+
}
149+
}
150+
}
151+
132152
/// <summary>
133153
/// This method runs several validations against the config file such as schema validation,
134154
/// validation of entities metadata, validation of permissions, validation of entity configuration.
@@ -1323,4 +1343,40 @@ private bool IsValidPermissionAction(EntityActionOperation action, Entity entity
13231343
return action is EntityActionOperation.All || EntityAction.ValidPermissionOperations.Contains(action);
13241344
}
13251345
}
1346+
1347+
/// <summary>
1348+
/// Returns whether the log-level keyword is valid or not.
1349+
/// It does this by checking each section of the name of the class,
1350+
/// in order to ensure that the last section is complete.
1351+
/// E.g. Azure.DataApiBuilder is valid. While Azure.DataA is invalid.
1352+
/// </summary>
1353+
/// <param name="loggerFilter">String keyword that comes from log-level in config file</param>
1354+
private static bool IsLoggerFilterValid(string loggerFilter)
1355+
{
1356+
string[] loggerSub = loggerFilter.Split('.');
1357+
for (int i = 0; i < LoggerFilters.validFilters.Count; i++)
1358+
{
1359+
bool isValid = true;
1360+
string[] validFiltersSub = LoggerFilters.validFilters[i].Split('.');
1361+
1362+
if (loggerSub.Length <= validFiltersSub.Length)
1363+
{
1364+
for (int j = 0; j < loggerSub.Length; j++)
1365+
{
1366+
if (!loggerSub[j].Equals(validFiltersSub[j]))
1367+
{
1368+
isValid = false;
1369+
break;
1370+
}
1371+
}
1372+
1373+
if (isValid)
1374+
{
1375+
return true;
1376+
}
1377+
}
1378+
}
1379+
1380+
return false;
1381+
}
13261382
}

0 commit comments

Comments
 (0)