Skip to content

Commit

Permalink
Add log-level property to config schema (#2359)
Browse files Browse the repository at this point in the history
## Why make this change?

Adds log-level property to dab config file under the runtime section,
closing issue #1645. The log-level is necessary to receive updates from
the program, and with this property, it gives the users the ability to
change what type of information they receive from the program. Lastly,
it allows the users to have more options on how to change the log-level
property, be it through the config file or the CLI.

## What is this change?

First, the log-level property was added to the config file schema, in
order for the user to have the ability to add the property into their
config file. In order to save the information that is inside the
property, a new object model named `LogLevelOptions` was created, inside
of it is a new object model named `Level` with the purpose of showing
all of the possible values in an enum type. A `LogLevelOptions` was then
added to `RuntimeOptions` so the program knows in which section it is
supposed to parse the information. A new converter
`LogLevelOptionsConverterFactory` was created in order to allow the
property in the config file to be deserialized. The logic in `Startup`
file was modified to first tries to set the loggers based on the
log-level property, in the case that log-level is null, it will fall
back on the logic that was set up for `Host Mode`. New tests were
created in `ConfigurationTests` to test the validity of the code.

## How was this tested?

- [x] Integration Tests
- [ ] Unit Tests

---------

Co-authored-by: Ruben Cerna <[email protected]>
  • Loading branch information
RubenCerna2079 and Ruben Cerna authored Sep 12, 2024
1 parent e9972fe commit 37222f1
Show file tree
Hide file tree
Showing 10 changed files with 244 additions and 20 deletions.
24 changes: 23 additions & 1 deletion schemas/dab.draft.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@
"description": "Configuration properties for multiple mutation operations",
"additionalProperties": false,
"properties": {
"create":{
"create": {
"type": "object",
"description": "Options for multiple create operations",
"additionalProperties": false,
Expand Down Expand Up @@ -261,6 +261,28 @@
}
}
},
"log-level": {
"type": "object",
"description": "Global configuration of log level",
"additionalProperties": false,
"properties": {
"level": {
"description": "Defines logging severity levels, when in default value it will set logging level based on 'host: mode' property",
"type": "string",
"default": null,
"enum": [
"trace",
"debug",
"information",
"warning",
"error",
"critical",
"none",
null
]
}
}
},
"cache": {
"type": "object",
"additionalProperties": false,
Expand Down
2 changes: 1 addition & 1 deletion src/Cli/ConfigGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1265,7 +1265,7 @@ public static bool TryStartEngineWithOptions(StartOptions options, FileSystemRun
}
else
{
minimumLogLevel = Startup.GetLogLevelBasedOnMode(deserializedRuntimeConfig);
minimumLogLevel = RuntimeConfig.GetConfiguredLogLevel(deserializedRuntimeConfig);
HostMode hostModeType = deserializedRuntimeConfig.IsDevelopmentMode() ? HostMode.Development : HostMode.Production;

_logger.LogInformation("Setting default minimum LogLevel: {minimumLogLevel} for {hostMode} mode.", minimumLogLevel, hostModeType);
Expand Down
49 changes: 49 additions & 0 deletions src/Config/Converters/LogLevelOptionsConverterFactory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System.Text.Json;
using System.Text.Json.Serialization;
using Azure.DataApiBuilder.Config.ObjectModel;

namespace Azure.DataApiBuilder.Config.Converters;

/// <summary>
/// Defines how DAB reads and writes log level options
/// </summary>
internal class LogLevelOptionsConverterFactory : JsonConverterFactory
{
/// <inheritdoc/>
public override bool CanConvert(Type typeToConvert)
{
return typeToConvert.IsAssignableTo(typeof(LogLevelOptions));
}

/// <inheritdoc/>
public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options)
{
return new LogLevelOptionsConverter();
}

private class LogLevelOptionsConverter : JsonConverter<LogLevelOptions>
{
/// <summary>
/// Defines how DAB reads loglevel options and defines which values are
/// used to instantiate LogLevelOptions.
/// Uses default deserialize.
/// </summary>
public override LogLevelOptions? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
JsonSerializerOptions jsonSerializerOptions = new(options);
jsonSerializerOptions.Converters.Remove(jsonSerializerOptions.Converters.First(c => c is LogLevelOptionsConverterFactory));
return JsonSerializer.Deserialize<LogLevelOptions>(ref reader, jsonSerializerOptions);
}

public override void Write(Utf8JsonWriter writer, LogLevelOptions value, JsonSerializerOptions options)
{
writer.WriteStartObject();
writer.WritePropertyName("level");
JsonSerializer.Serialize(writer, value.Value, options);
writer.WriteEndObject();
}
}
}
21 changes: 21 additions & 0 deletions src/Config/ObjectModel/LogLevelOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System.Text.Json.Serialization;
using Microsoft.Extensions.Logging;

namespace Azure.DataApiBuilder.Config.ObjectModel;

/// <summary>
/// Holds the settings used at runtime to set the LogLevel of the different providers
/// </summary>
public record LogLevelOptions
{
[JsonPropertyName("level")]
public LogLevel? Value { get; set; }

public LogLevelOptions(LogLevel? Value = null)
{
this.Value = Value;
}
}
32 changes: 32 additions & 0 deletions src/Config/ObjectModel/RuntimeConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using Azure.DataApiBuilder.Service.Exceptions;
using Microsoft.Extensions.Logging;

namespace Azure.DataApiBuilder.Config.ObjectModel;

Expand Down Expand Up @@ -536,4 +537,35 @@ public uint GetPaginationLimit(int? first)
return defaultPageSize;
}
}

/// <summary>
/// Checks if the property log-level or its value are null
/// </summary>
public bool IsLogLevelNull() =>
Runtime is null ||
Runtime.LoggerLevel is null ||
Runtime.LoggerLevel.Value is null;

/// <summary>
/// Takes in the RuntimeConfig object and checks the LogLevel.
/// If LogLevel is not null, it will return the current value as a LogLevel,
/// else it will take the default option by checking host mode.
/// If host mode is Development, return `LogLevel.Debug`, else
/// for production returns `LogLevel.Error`.
/// </summary>
public static LogLevel GetConfiguredLogLevel(RuntimeConfig runtimeConfig)
{
LogLevel? value = runtimeConfig.Runtime?.LoggerLevel?.Value;
if (value is not null)
{
return (LogLevel)value;
}

if (runtimeConfig.IsDevelopmentMode())
{
return LogLevel.Debug;
}

return LogLevel.Error;
}
}
7 changes: 6 additions & 1 deletion src/Config/ObjectModel/RuntimeOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ public record RuntimeOptions
public EntityCacheOptions? Cache { get; init; }
public PaginationOptions? Pagination { get; init; }

[JsonPropertyName("log-level")]
public LogLevelOptions? LoggerLevel { get; init; }

[JsonConstructor]
public RuntimeOptions(
RestRuntimeOptions? Rest,
Expand All @@ -24,7 +27,8 @@ public RuntimeOptions(
string? BaseRoute = null,
TelemetryOptions? Telemetry = null,
EntityCacheOptions? Cache = null,
PaginationOptions? Pagination = null)
PaginationOptions? Pagination = null,
LogLevelOptions? LoggerLevel = null)
{
this.Rest = Rest;
this.GraphQL = GraphQL;
Expand All @@ -33,6 +37,7 @@ public RuntimeOptions(
this.Telemetry = Telemetry;
this.Cache = Cache;
this.Pagination = Pagination;
this.LoggerLevel = LoggerLevel;
}

/// <summary>
Expand Down
1 change: 1 addition & 0 deletions src/Config/RuntimeConfigLoader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,7 @@ public static JsonSerializerOptions GetSerializationOptions(
options.Converters.Add(new MultipleMutationOptionsConverter(options));
options.Converters.Add(new DataSourceConverterFactory(replaceEnvVar));
options.Converters.Add(new HostOptionsConvertorFactory());
options.Converters.Add(new LogLevelOptionsConverterFactory());

if (replaceEnvVar)
{
Expand Down
108 changes: 108 additions & 0 deletions src/Service.Tests/Configuration/ConfigurationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3450,6 +3450,114 @@ public async Task OpenApi_InteractiveSwaggerUI(
}
}

/// <summary>
/// Test different loglevel values that are avaliable by deserializing RuntimeConfig with specified LogLevel
/// and checks if value exists properly inside the deserialized RuntimeConfig.
/// </summary>
[DataTestMethod]
[TestCategory(TestCategory.MSSQL)]
[DataRow(LogLevel.Trace, DisplayName = "Validates that log level Trace deserialized correctly")]
[DataRow(LogLevel.Debug, DisplayName = "Validates log level Debug deserialized correctly")]
[DataRow(LogLevel.Information, DisplayName = "Validates log level Information deserialized correctly")]
[DataRow(LogLevel.Warning, DisplayName = "Validates log level Warning deserialized correctly")]
[DataRow(LogLevel.Error, DisplayName = "Validates log level Error deserialized correctly")]
[DataRow(LogLevel.Critical, DisplayName = "Validates log level Critical deserialized correctly")]
[DataRow(LogLevel.None, DisplayName = "Validates log level None deserialized correctly")]
[DataRow(null, DisplayName = "Validates log level Null deserialized correctly")]
public void TestExistingLogLevels(LogLevel expectedLevel)
{
RuntimeConfig configWithCustomLogLevel = InitializeRuntimeWithLogLevel(expectedLevel);

string configWithCustomLogLevelJson = configWithCustomLogLevel.ToJson();
Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(configWithCustomLogLevelJson, out RuntimeConfig deserializedRuntimeConfig));

Assert.AreEqual(expectedLevel, deserializedRuntimeConfig.Runtime.LoggerLevel.Value);
}

/// <summary>
/// Test different loglevel values that do not exist to ensure that the build fails when they are trying to be set up
/// </summary>
[DataTestMethod]
[TestCategory(TestCategory.MSSQL)]
[DataRow(-1, DisplayName = "Validates that a negative log level value, fails to build")]
[DataRow(7, DisplayName = "Validates that a positive log level value that does not exist, fails to build")]
[DataRow(12, DisplayName = "Validates that a bigger positive log level value that does not exist, fails to build")]
public void TestNonExistingLogLevels(LogLevel expectedLevel)
{
RuntimeConfig configWithCustomLogLevel = InitializeRuntimeWithLogLevel(expectedLevel);

// Try should fail and go to catch exception
try
{
string configWithCustomLogLevelJson = configWithCustomLogLevel.ToJson();
Assert.Fail();
}
// Catch verifies that the exception is due to LogLevel having a value that does not exist
catch (Exception ex)
{
Assert.AreEqual(typeof(KeyNotFoundException), ex.GetType());
}
}

/// <summary>
/// Tests different loglevel values to see if they are serialized correctly to the Json config
/// </summary>
[DataTestMethod]
[TestCategory(TestCategory.MSSQL)]
[DataRow(LogLevel.Debug)]
[DataRow(LogLevel.Warning)]
[DataRow(LogLevel.None)]
[DataRow(null)]
public void LogLevelSerialization(LogLevel expectedLevel)
{
RuntimeConfig configWithCustomLogLevel = InitializeRuntimeWithLogLevel(expectedLevel);
string configWithCustomLogLevelJson = configWithCustomLogLevel.ToJson();
Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(configWithCustomLogLevelJson, out RuntimeConfig deserializedRuntimeConfig));

string serializedConfig = deserializedRuntimeConfig.ToJson();

using (JsonDocument parsedDocument = JsonDocument.Parse(serializedConfig))
{
JsonElement root = parsedDocument.RootElement;

//Validate log-level property exists in runtime
JsonElement runtimeElement = root.GetProperty("runtime");
bool logLevelPropertyExists = runtimeElement.TryGetProperty("log-level", out JsonElement logLevelElement);
Assert.AreEqual(expected: true, actual: logLevelPropertyExists);

//Validate level property inside log-level is of expected value
bool levelPropertyExists = logLevelElement.TryGetProperty("level", out JsonElement levelElement);
Assert.AreEqual(expected: true, actual: levelPropertyExists);
Assert.AreEqual(expectedLevel.ToString().ToLower(), levelElement.GetString());
}
}

/// <summary>
/// Helper method to create RuntimeConfig with specificed LogLevel value
/// </summary>
private static RuntimeConfig InitializeRuntimeWithLogLevel(LogLevel? expectedLevel)
{
TestHelper.SetupDatabaseEnvironment(MSSQL_ENVIRONMENT);

FileSystemRuntimeConfigLoader baseLoader = TestHelper.GetRuntimeConfigLoader();
baseLoader.TryLoadKnownConfig(out RuntimeConfig baseConfig);

LogLevelOptions logLevelOptions = new(Value: expectedLevel);
RuntimeConfig config = new(
Schema: baseConfig.Schema,
DataSource: baseConfig.DataSource,
Runtime: new(
Rest: new(),
GraphQL: new(),
Host: new(null, null),
LoggerLevel: logLevelOptions
),
Entities: baseConfig.Entities
);

return config;
}

/// <summary>
/// Validates the OpenAPI documentor behavior when enabling and disabling the global REST endpoint
/// for the DAB engine.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,7 @@ public void TestNullableOptionalProps()

// Test with empty sub properties of runtime
minJson.Append(@"{ ""rest"": { }, ""graphql"": { },
""base-route"" : """",");
""base-route"" : """", ""log-level"" : { },");
StringBuilder minJsonWithHostSubProps = new(minJson + @"""telemetry"" : { }, ""host"" : ");
StringBuilder minJsonWithTelemetrySubProps = new(minJson + @"""host"" : { }, ""telemetry"" : ");

Expand Down Expand Up @@ -645,6 +645,7 @@ private static bool TryParseAndAssertOnDefaults(string json, out RuntimeConfig p
Assert.IsFalse(parsedConfig.IsDevelopmentMode());
Assert.IsTrue(parsedConfig.IsStaticWebAppsIdentityProvider);
Assert.IsTrue(parsedConfig.IsRequestBodyStrict);
Assert.IsTrue(parsedConfig.IsLogLevelNull());
Assert.IsTrue(parsedConfig.Runtime?.Telemetry?.ApplicationInsights is null
|| !parsedConfig.Runtime.Telemetry.ApplicationInsights.Enabled);
return true;
Expand Down
17 changes: 1 addition & 16 deletions src/Service/Startup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -413,21 +413,6 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env, RuntimeC
});
}

/// <summary>
/// Takes in the RuntimeConfig object and checks the host mode.
/// If host mode is Development, return `LogLevel.Debug`, else
/// for production returns `LogLevel.Error`.
/// </summary>
public static LogLevel GetLogLevelBasedOnMode(RuntimeConfig runtimeConfig)
{
if (runtimeConfig.IsDevelopmentMode())
{
return LogLevel.Debug;
}

return LogLevel.Error;
}

/// <summary>
/// If LogLevel is NOT overridden by CLI, attempts to find the
/// minimum log level based on host.mode in the runtime config if available.
Expand All @@ -444,7 +429,7 @@ public static ILoggerFactory CreateLoggerFactoryForHostedAndNonHostedScenario(IS
RuntimeConfigProvider configProvider = serviceProvider.GetRequiredService<RuntimeConfigProvider>();
if (configProvider.TryGetConfig(out RuntimeConfig? runtimeConfig))
{
MinimumLogLevel = GetLogLevelBasedOnMode(runtimeConfig);
MinimumLogLevel = RuntimeConfig.GetConfiguredLogLevel(runtimeConfig);
}
}

Expand Down

0 comments on commit 37222f1

Please sign in to comment.