diff --git a/schemas/dab.draft.schema.json b/schemas/dab.draft.schema.json index 692b82b642..5ab627fa77 100644 --- a/schemas/dab.draft.schema.json +++ b/schemas/dab.draft.schema.json @@ -410,6 +410,47 @@ }, "required": ["endpoint"] }, + "azure-log-analytics": { + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { + "type": "boolean", + "description": "Allow enabling/disabling Azure Log Analytics.", + "default": false + }, + "auth": { + "type": "object", + "additionalProperties": false, + "properties": { + "workspace-id": { + "type": "string", + "description": "Azure Log Analytics Workspace ID" + }, + "dcr-immutable-id": { + "type": "string", + "description": "DCR ID for entra-id mode" + }, + "dce-endpoint": { + "type": "string", + "description": "DCE endpoint for entra-id mode" + } + }, + "required": ["workspace-id"] + }, + "log-type": { + "type": "string", + "description": "Custom log table name in Log Analytics", + "default": "DabLogs" + }, + "flush-interval-seconds": { + "type": "integer", + "description": "Interval between log batch pushes (in seconds)", + "default": 5 + } + }, + "required": ["auth"] + }, "log-level": { "type": "object", "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", diff --git a/src/Config/Converters/AzureLogAnalyticsAuthOptionsConverter.cs b/src/Config/Converters/AzureLogAnalyticsAuthOptionsConverter.cs new file mode 100644 index 0000000000..c17cedfe6f --- /dev/null +++ b/src/Config/Converters/AzureLogAnalyticsAuthOptionsConverter.cs @@ -0,0 +1,142 @@ +// 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; + +internal class AzureLogAnalyticsAuthOptionsConverter : JsonConverter +{ + // Determines whether to replace environment variable with its + // value or not while deserializing. + private bool _replaceEnvVar; + + /// Whether to replace environment variable with its + /// value or not while deserializing. + public AzureLogAnalyticsAuthOptionsConverter(bool replaceEnvVar) + { + _replaceEnvVar = replaceEnvVar; + } + + /// + /// Defines how DAB reads Azure Log Analytics Auth options and defines which values are + /// used to instantiate AzureLogAnalyticsAuthOptions. + /// Uses default deserialize. + /// + /// Thrown when improperly formatted Azure Log Analytics Auth options are provided. + public override AzureLogAnalyticsAuthOptions? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType is JsonTokenType.StartObject) + { + AzureLogAnalyticsAuthOptions? authOptions = new(); + + while (reader.Read()) + { + if (reader.TokenType == JsonTokenType.EndObject) + { + break; + } + + string? propertyName = reader.GetString(); + + reader.Read(); + switch (propertyName) + { + case "workspace-id": + if (reader.TokenType is JsonTokenType.String) + { + string? workspaceId = reader.DeserializeString(_replaceEnvVar); + if (workspaceId is null) + { + throw new JsonException($"Unsuported null value entered for the property workspace-id"); + } + + authOptions = authOptions with { WorkspaceId = workspaceId }; + } + else + { + throw new JsonException($"Unexpected type of value entered for workspace-id: {reader.TokenType}"); + } + + break; + + case "dcr-immutable-id": + if (reader.TokenType is JsonTokenType.String) + { + string? dcrImmutableId = reader.DeserializeString(_replaceEnvVar); + if (dcrImmutableId is null) + { + throw new JsonException($"Unsuported null value entered for the property dcr-immutable-id"); + } + + authOptions = authOptions with { DcrImmutableId = dcrImmutableId }; + } + else + { + throw new JsonException($"Unexpected type of value entered for dcr-immutable-id: {reader.TokenType}"); + } + + break; + + case "dce-endpoint": + if (reader.TokenType is JsonTokenType.String) + { + string? dceEndpoint = reader.DeserializeString(_replaceEnvVar); + if (dceEndpoint is null) + { + throw new JsonException($"Unsuported null value entered for the property dce-endpoint"); + } + + authOptions = authOptions with { DceEndpoint = dceEndpoint }; + } + else + { + throw new JsonException($"Unexpected type of value entered for dce-endpoint: {reader.TokenType}"); + } + + break; + + default: + throw new JsonException($"Unexpected property {propertyName}"); + } + } + + return authOptions; + } + + throw new JsonException("Failed to read the Azure Log Analytics Auth Options"); + } + + /// + /// When writing the AzureLogAnalyticsAuthOptions back to a JSON file, only write the properties + /// if they are user provided. This avoids polluting the written JSON file with properties + /// the user most likely omitted when writing the original DAB runtime config file. + /// This Write operation is only used when a RuntimeConfig object is serialized to JSON. + /// + public override void Write(Utf8JsonWriter writer, AzureLogAnalyticsAuthOptions value, JsonSerializerOptions options) + { + writer.WriteStartObject(); + + if (value?.UserProvidedWorkspaceId is true) + { + writer.WritePropertyName("workspace-id"); + JsonSerializer.Serialize(writer, value.WorkspaceId, options); + } + + if (value?.UserProvidedDcrImmutableId is true) + { + writer.WritePropertyName("drc-immutable-id"); + JsonSerializer.Serialize(writer, value.DcrImmutableId, options); + } + + if (value?.UserProvidedDceEndpoint is true) + { + writer.WritePropertyName("dce-endpoint"); + JsonSerializer.Serialize(writer, value.DceEndpoint, options); + } + + writer.WriteEndObject(); + } +} diff --git a/src/Config/Converters/AzureLogAnalyticsOptionsConverterFactory.cs b/src/Config/Converters/AzureLogAnalyticsOptionsConverterFactory.cs new file mode 100644 index 0000000000..6d3faef9bf --- /dev/null +++ b/src/Config/Converters/AzureLogAnalyticsOptionsConverterFactory.cs @@ -0,0 +1,187 @@ +// 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; + +/// +/// Defines how DAB reads and writes Azure Log Analytics options. +/// +internal class AzureLogAnalyticsOptionsConverterFactory : JsonConverterFactory +{ + // Determines whether to replace environment variable with its + // value or not while deserializing. + private bool _replaceEnvVar; + + /// + public override bool CanConvert(Type typeToConvert) + { + return typeToConvert.IsAssignableTo(typeof(AzureLogAnalyticsOptions)); + } + + /// + public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) + { + return new AzureLogAnalyticsOptionsConverter(_replaceEnvVar); + } + + /// Whether to replace environment variable with its + /// value or not while deserializing. + internal AzureLogAnalyticsOptionsConverterFactory(bool replaceEnvVar) + { + _replaceEnvVar = replaceEnvVar; + } + + private class AzureLogAnalyticsOptionsConverter : JsonConverter + { + // Determines whether to replace environment variable with its + // value or not while deserializing. + private bool _replaceEnvVar; + + /// Whether to replace environment variable with its + /// value or not while deserializing. + internal AzureLogAnalyticsOptionsConverter(bool replaceEnvVar) + { + _replaceEnvVar = replaceEnvVar; + } + + /// + /// Defines how DAB reads Azure Log Analytics options and defines which values are + /// used to instantiate AzureLogAnalyticsOptions. + /// Uses default deserialize. + /// + /// Thrown when improperly formatted Azure Log Analytics options are provided. + public override AzureLogAnalyticsOptions? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType is JsonTokenType.StartObject) + { + AzureLogAnalyticsOptions azureLogAnalyticsOptions = new(); + AzureLogAnalyticsAuthOptionsConverter authOptionsConverter = new(_replaceEnvVar); + + while (reader.Read()) + { + if (reader.TokenType == JsonTokenType.EndObject) + { + break; + } + + string? propertyName = reader.GetString(); + + reader.Read(); + switch (propertyName) + { + case "enabled": + if (reader.TokenType is JsonTokenType.True || reader.TokenType is JsonTokenType.False) + { + azureLogAnalyticsOptions = azureLogAnalyticsOptions with { Enabled = reader.GetBoolean() }; + } + else + { + throw new JsonException($"Unsupported value entered for the property 'enabled': {reader.TokenType}"); + } + + break; + + case "auth": + azureLogAnalyticsOptions = azureLogAnalyticsOptions with { Auth = authOptionsConverter.Read(ref reader, typeToConvert, options) }; + break; + + case "log-type": + if (reader.TokenType is JsonTokenType.String) + { + string? logType = reader.DeserializeString(_replaceEnvVar); + if (logType is null) + { + logType = "DabLogs"; + } + + azureLogAnalyticsOptions = azureLogAnalyticsOptions with { LogType = logType }; + } + else + { + throw new JsonException($"Unexpected type of value entered for log-type: {reader.TokenType}"); + } + + break; + + case "flush-interval-seconds": + if (reader.TokenType is JsonTokenType.Number) + { + int flushIntSec; + try + { + flushIntSec = reader.GetInt32(); + } + catch (FormatException) + { + throw new JsonException($"The JSON token value is of the incorrect numeric format."); + } + + if (flushIntSec <= 0) + { + throw new JsonException($"Invalid flush-interval-seconds: {flushIntSec}. Specify a number > 0."); + } + + azureLogAnalyticsOptions = azureLogAnalyticsOptions with { FlushIntervalSeconds = flushIntSec }; + } + else + { + throw new JsonException($"Unsupported value entered for flush-interval-seconds: {reader.TokenType}"); + } + + break; + + default: + throw new JsonException($"Unexpected property {propertyName}"); + } + } + + return azureLogAnalyticsOptions; + } + + throw new JsonException("Failed to read the Azure Log Analytics Options"); + } + + /// + /// When writing the AzureLogAnalyticsOptions back to a JSON file, only write the properties + /// if they are user provided. This avoids polluting the written JSON file with properties + /// the user most likely omitted when writing the original DAB runtime config file. + /// This Write operation is only used when a RuntimeConfig object is serialized to JSON. + /// + public override void Write(Utf8JsonWriter writer, AzureLogAnalyticsOptions value, JsonSerializerOptions options) + { + writer.WriteStartObject(); + + if (value?.UserProvidedEnabled is true) + { + writer.WritePropertyName("enabled"); + JsonSerializer.Serialize(writer, value.Enabled, options); + } + + if (value?.Auth is not null) + { + AzureLogAnalyticsAuthOptionsConverter authOptionsConverter = options.GetConverter(typeof(AzureLogAnalyticsAuthOptions)) as AzureLogAnalyticsAuthOptionsConverter ?? + throw new JsonException("Failed to get azure-log-analytics.auth options converter"); + + authOptionsConverter.Write(writer, value.Auth, options); + } + + if (value?.UserProvidedLogType is true) + { + writer.WritePropertyName("log-type"); + JsonSerializer.Serialize(writer, value.LogType, options); + } + + if (value?.UserProvidedFlushIntervalSeconds is true) + { + writer.WritePropertyName("flush-interval-seconds"); + JsonSerializer.Serialize(writer, value.FlushIntervalSeconds, options); + } + + writer.WriteEndObject(); + } + } +} diff --git a/src/Config/ObjectModel/AzureLogAnalyticsAuthOptions.cs b/src/Config/ObjectModel/AzureLogAnalyticsAuthOptions.cs new file mode 100644 index 0000000000..cc8ed9dffa --- /dev/null +++ b/src/Config/ObjectModel/AzureLogAnalyticsAuthOptions.cs @@ -0,0 +1,80 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Diagnostics.CodeAnalysis; +using System.Text.Json.Serialization; + +namespace Azure.DataApiBuilder.Config.ObjectModel; + +/// +/// Represents the authentication options for Azure Log Analytics. +/// +public record AzureLogAnalyticsAuthOptions +{ + /// + /// Whether Azure Log Analytics is enabled. + /// + public string? WorkspaceId { get; init; } + + /// + /// Authentication options for Azure Log Analytics. + /// + public string? DcrImmutableId { get; init; } + + /// + /// Custom log table name in Log Analytics. + /// + public string? DceEndpoint { get; init; } + + [JsonConstructor] + public AzureLogAnalyticsAuthOptions(string? workspaceId = null, string? dcrImmutableId = null, string? dceEndpoint = null) + { + if (workspaceId is not null) + { + WorkspaceId = workspaceId; + UserProvidedWorkspaceId = true; + } + + if (dcrImmutableId is not null) + { + DcrImmutableId = dcrImmutableId; + UserProvidedDcrImmutableId = true; + } + + if (dceEndpoint is not null) + { + DceEndpoint = dceEndpoint; + UserProvidedDceEndpoint = true; + } + } + + /// + /// Flag which informs CLI and JSON serializer whether to write workspace-id + /// property and value to the runtime config file. + /// When user doesn't provide the workspace-id property/value, which signals DAB to not write anything, + /// the DAB CLI should not write the current value to a serialized config. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.Always)] + [MemberNotNullWhen(true, nameof(WorkspaceId))] + public bool UserProvidedWorkspaceId { get; init; } = false; + + /// + /// Flag which informs CLI and JSON serializer whether to write dcr-immutable-id + /// property and value to the runtime config file. + /// When user doesn't provide the dcr-immutable-id property/value, which signals DAB to not write anything, + /// the DAB CLI should not write the current value to a serialized config. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.Always)] + [MemberNotNullWhen(true, nameof(DcrImmutableId))] + public bool UserProvidedDcrImmutableId { get; init; } = false; + + /// + /// Flag which informs CLI and JSON serializer whether to write dce-endpoint + /// property and value to the runtime config file. + /// When user doesn't provide the dce-endpoint property/value, which signals DAB to not write anything, + /// the DAB CLI should not write the current value to a serialized config. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.Always)] + [MemberNotNullWhen(true, nameof(DceEndpoint))] + public bool UserProvidedDceEndpoint { get; init; } = false; +} diff --git a/src/Config/ObjectModel/AzureLogAnalyticsOptions.cs b/src/Config/ObjectModel/AzureLogAnalyticsOptions.cs new file mode 100644 index 0000000000..75e7f225b5 --- /dev/null +++ b/src/Config/ObjectModel/AzureLogAnalyticsOptions.cs @@ -0,0 +1,107 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Diagnostics.CodeAnalysis; +using System.Text.Json.Serialization; + +namespace Azure.DataApiBuilder.Config.ObjectModel; + +/// +/// Represents the options for configuring Azure Log Analytics. +/// Properties are nullable to support DAB CLI merge config +/// expected behavior. +/// +public record AzureLogAnalyticsOptions +{ + /// + /// Default log type for Azure Log Analytics. + /// + public const string DEFAULT_LOG_TYPE = "DabLogs"; + + /// + /// Default flush interval in seconds. + /// + public const int DEFAULT_FLUSH_INTERVAL_SECONDS = 5; + + /// + /// Whether Azure Log Analytics is enabled. + /// + public bool Enabled { get; init; } + + /// + /// Authentication options for Azure Log Analytics. + /// + public AzureLogAnalyticsAuthOptions? Auth { get; init; } + + /// + /// Custom log table name in Log Analytics. + /// + public string? LogType { get; init; } + + /// + /// Interval between log batch pushes (in seconds). + /// + public int? FlushIntervalSeconds { get; init; } + + [JsonConstructor] + public AzureLogAnalyticsOptions(bool enabled = false, AzureLogAnalyticsAuthOptions? auth = null, string? logType = null, int? flushIntervalSeconds = null) + { + Auth = auth; + + if (enabled) + { + Enabled = enabled; + UserProvidedEnabled = true; + } + + if (logType is not null) + { + LogType = logType; + UserProvidedLogType = true; + } + else + { + LogType = DEFAULT_LOG_TYPE; + } + + if (flushIntervalSeconds is not null) + { + FlushIntervalSeconds = flushIntervalSeconds; + UserProvidedFlushIntervalSeconds = true; + } + else + { + FlushIntervalSeconds = DEFAULT_FLUSH_INTERVAL_SECONDS; + } + } + + /// + /// Flag which informs CLI and JSON serializer whether to write enabled + /// property and value to the runtime config file. + /// When user doesn't provide the enabled property/value, which signals DAB to use the default, + /// the DAB CLI should not write the default value to a serialized config. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.Always)] + [MemberNotNullWhen(true, nameof(Enabled))] + public bool UserProvidedEnabled { get; init; } = false; + + /// + /// Flag which informs CLI and JSON serializer whether to write log-type + /// property and value to the runtime config file. + /// When user doesn't provide the log-type property/value, which signals DAB to use the default, + /// the DAB CLI should not write the default value to a serialized config. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.Always)] + [MemberNotNullWhen(true, nameof(LogType))] + public bool UserProvidedLogType { get; init; } = false; + + /// + /// Flag which informs CLI and JSON serializer whether to write flush-interval-seconds + /// property and value to the runtime config file. + /// When user doesn't provide the flush-interval-seconds property/value, which signals DAB to use the default, + /// the DAB CLI should not write the default value to a serialized config. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.Always)] + [MemberNotNullWhen(true, nameof(FlushIntervalSeconds))] + public bool UserProvidedFlushIntervalSeconds { get; init; } = false; +} diff --git a/src/Config/ObjectModel/TelemetryOptions.cs b/src/Config/ObjectModel/TelemetryOptions.cs index ed2099f2a4..157b0d03b2 100644 --- a/src/Config/ObjectModel/TelemetryOptions.cs +++ b/src/Config/ObjectModel/TelemetryOptions.cs @@ -9,7 +9,11 @@ namespace Azure.DataApiBuilder.Config.ObjectModel; /// /// Represents the options for telemetry. /// -public record TelemetryOptions(ApplicationInsightsOptions? ApplicationInsights = null, OpenTelemetryOptions? OpenTelemetry = null, Dictionary? LoggerLevel = null) +public record TelemetryOptions( + ApplicationInsightsOptions? ApplicationInsights = null, + OpenTelemetryOptions? OpenTelemetry = null, + AzureLogAnalyticsOptions? AzureLogAnalytics = null, + Dictionary? LoggerLevel = null) { [JsonPropertyName("log-level")] public Dictionary? LoggerLevel { get; init; } = LoggerLevel; diff --git a/src/Config/RuntimeConfigLoader.cs b/src/Config/RuntimeConfigLoader.cs index b4f72335c3..f087a7aa9a 100644 --- a/src/Config/RuntimeConfigLoader.cs +++ b/src/Config/RuntimeConfigLoader.cs @@ -258,6 +258,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 AzureLogAnalyticsOptionsConverterFactory(replaceEnvVar)); if (replaceEnvVar) { diff --git a/src/Core/Configurations/RuntimeConfigValidator.cs b/src/Core/Configurations/RuntimeConfigValidator.cs index 4d293d0cd2..ab855ade79 100644 --- a/src/Core/Configurations/RuntimeConfigValidator.cs +++ b/src/Core/Configurations/RuntimeConfigValidator.cs @@ -74,6 +74,7 @@ public void ValidateConfigProperties() ValidateGlobalEndpointRouteConfig(runtimeConfig); ValidateAppInsightsTelemetryConnectionString(runtimeConfig); ValidateLoggerFilters(runtimeConfig); + ValidateAzureLogAnalyticsAuth(runtimeConfig); // Running these graphQL validations only in development mode to ensure // fast startup of engine in production mode. @@ -149,6 +150,26 @@ public static void ValidateLoggerFilters(RuntimeConfig runtimeConfig) } } + /// + /// The auth options in Azure Log Analytics are required if it is enabled. + /// + public void ValidateAzureLogAnalyticsAuth(RuntimeConfig runtimeConfig) + { + if (runtimeConfig.Runtime!.Telemetry is not null && runtimeConfig.Runtime.Telemetry.AzureLogAnalytics is not null) + { + AzureLogAnalyticsOptions azureLogAnalyticsOptions = runtimeConfig.Runtime.Telemetry.AzureLogAnalytics; + AzureLogAnalyticsAuthOptions? azureLogAnalyticsAuthOptions = azureLogAnalyticsOptions.Auth; + if (azureLogAnalyticsOptions.Enabled && (azureLogAnalyticsAuthOptions is null || string.IsNullOrWhiteSpace(azureLogAnalyticsAuthOptions.WorkspaceId) || + string.IsNullOrWhiteSpace(azureLogAnalyticsAuthOptions.DcrImmutableId) || string.IsNullOrWhiteSpace(azureLogAnalyticsAuthOptions.DceEndpoint))) + { + HandleOrRecordException(new DataApiBuilderException( + message: "Azure Log Analytics Auth options 'workspace-id', 'dcr-immutable-id', and 'dce-endpoint' cannot be null or empty if enabled.", + statusCode: HttpStatusCode.ServiceUnavailable, + subStatusCode: DataApiBuilderException.SubStatusCodes.ConfigValidationError)); + } + } + } + /// /// This method runs several validations against the config file such as schema validation, /// validation of entities metadata, validation of permissions, validation of entity configuration. diff --git a/src/Service.Tests/Configuration/AzureLogAnalyticsConfigurationTests.cs b/src/Service.Tests/Configuration/AzureLogAnalyticsConfigurationTests.cs new file mode 100644 index 0000000000..251ee5283b --- /dev/null +++ b/src/Service.Tests/Configuration/AzureLogAnalyticsConfigurationTests.cs @@ -0,0 +1,162 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.IO; +using Azure.DataApiBuilder.Config.ObjectModel; +using Microsoft.AspNetCore.TestHost; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using static Azure.DataApiBuilder.Service.Tests.Configuration.ConfigurationTests; + +namespace Azure.DataApiBuilder.Service.Tests.Configuration; + +[TestClass, TestCategory(TestCategory.MSSQL)] +public class AzureLogAnalyticsConfigurationTests +{ + private const string CONFIG_WITH_TELEMETRY = "dab-azure-log-analytics-test-config.json"; + private const string CONFIG_WITHOUT_TELEMETRY = "dab-no-azure-log-analytics-test-config.json"; + private static RuntimeConfig _configuration; + + /// + /// Creates runtime config file with specified Azure Log Analytics telemetry options. + /// + /// Name of the config file to be created. + /// Whether Azure Log Analytics telemetry is enabled or not. + /// Azure Log Analytics workspace ID. + /// Custom log table name. + /// Flush interval in seconds. + public static void SetUpAzureLogAnalyticsInConfig(string configFileName, bool isTelemetryEnabled, string workspaceId, string dcrId = "test-dcr-id", string dceEndpoint = "test-dce-endpoint", string logType = "DabLogs", int flushIntervalSeconds = 5) + { + DataSource dataSource = new(DatabaseType.MSSQL, + GetConnectionStringFromEnvironmentConfig(environment: TestCategory.MSSQL), Options: null); + + _configuration = InitMinimalRuntimeConfig(dataSource, graphqlOptions: new(), restOptions: new()); + + AzureLogAnalyticsAuthOptions authOptions = new(workspaceId, dcrId, dceEndpoint); + AzureLogAnalyticsOptions azureLogAnalyticsOptions = new(isTelemetryEnabled, authOptions, logType, flushIntervalSeconds); + TelemetryOptions _testTelemetryOptions = new(AzureLogAnalytics: azureLogAnalyticsOptions); + _configuration = _configuration with { Runtime = _configuration.Runtime with { Telemetry = _testTelemetryOptions } }; + + File.WriteAllText(configFileName, _configuration.ToJson()); + } + + /// + /// Cleans up the test environment by deleting the runtime config with telemetry options. + /// + [TestCleanup] + public void CleanUpTelemetryConfig() + { + if (File.Exists(CONFIG_WITH_TELEMETRY)) + { + File.Delete(CONFIG_WITH_TELEMETRY); + } + + if (File.Exists(CONFIG_WITHOUT_TELEMETRY)) + { + File.Delete(CONFIG_WITHOUT_TELEMETRY); + } + + } + /// + /// Tests if the services are correctly enabled for Open Telemetry. + /// + /// NOTE: This tests are still not finished, they will be completed in the next PR when the connection to Azure Log Analytics is completed + [TestMethod] + [Ignore] + public void TestOpenTelemetryServicesEnabled() + { + // Arrange + SetUpAzureLogAnalyticsInConfig(CONFIG_WITH_TELEMETRY, true, "http://localhost:4317"); + + string[] args = new[] + { + $"--ConfigFileName={CONFIG_WITH_TELEMETRY}" + }; + using TestServer server = new(Program.CreateWebHostBuilder(args)); + } + + /// + /// Tests if the services are correctly disabled for Open Telemetry. + /// + /// NOTE: This tests are still not finished, they will be completed in the next PR when the connection to Azure Log Analytics is completed + [TestMethod] + [Ignore] + public void TestOpenTelemetryServicesDisabled() + { + // Arrange + SetUpAzureLogAnalyticsInConfig(CONFIG_WITHOUT_TELEMETRY, false, null, null, null, null); + + string[] args = new[] + { + $"--ConfigFileName={CONFIG_WITHOUT_TELEMETRY}" + }; + using TestServer server = new(Program.CreateWebHostBuilder(args)); + } + + /// + /// Test that Azure Log Analytics options serialize only user-provided properties. + /// + [TestMethod] + [Ignore] + public void TestAzureLogAnalyticsOptionsSerializationWithUserProvidedFlags() + { + // Arrange - Create config with only explicitly provided auth options + DataSource dataSource = new(DatabaseType.MSSQL, + GetConnectionStringFromEnvironmentConfig(environment: TestCategory.MSSQL), Options: null); + + RuntimeConfig config = InitMinimalRuntimeConfig(dataSource, graphqlOptions: new(), restOptions: new()); + + // Only provide auth options, other properties should use defaults but not be serialized + AzureLogAnalyticsAuthOptions authOptions = new("test-workspace-id", "test-dcr-id", "test-dce-endpoint"); + AzureLogAnalyticsOptions azureLogAnalyticsOptions = new(auth: authOptions); + TelemetryOptions telemetryOptions = new(AzureLogAnalytics: azureLogAnalyticsOptions); + config = config with { Runtime = config.Runtime with { Telemetry = telemetryOptions } }; + + // Act - Serialize to JSON + string json = config.ToJson(); + + // Assert - azure-log-analytics section exists + // Check within the azure-log-analytics section specifically + Assert.IsTrue(json.Contains("\"azure-log-analytics\""), "Azure log analytics section should exist"); + + // Extract just the azure-log-analytics section for more precise checks + int startIndex = json.IndexOf("\"azure-log-analytics\""); + int openBrace = json.IndexOf('{', startIndex); + int closeBrace = json.IndexOf('}', openBrace); + string azureLogAnalyticsSection = json.Substring(openBrace, closeBrace - openBrace + 1); + + Assert.IsTrue(azureLogAnalyticsSection.Contains("\"auth\""), "Auth options should be included in serialized JSON"); + Assert.IsFalse(azureLogAnalyticsSection.Contains("\"enabled\""), "Enabled should not be included when using default value"); + Assert.IsFalse(azureLogAnalyticsSection.Contains("\"log-type\""), "Log-type should not be included when using default value"); + Assert.IsFalse(azureLogAnalyticsSection.Contains("\"flush-interval-seconds\""), "Flush-interval-seconds should not be included when using default value"); + } + + /// + /// Test that Azure Log Analytics options serialize all properties when explicitly provided. + /// + [TestMethod] + [Ignore] + public void TestAzureLogAnalyticsOptionsSerializationWithAllPropertiesProvided() + { + // Arrange - Create config with all properties explicitly provided + DataSource dataSource = new(DatabaseType.MSSQL, + GetConnectionStringFromEnvironmentConfig(environment: TestCategory.MSSQL), Options: null); + + RuntimeConfig config = InitMinimalRuntimeConfig(dataSource, graphqlOptions: new(), restOptions: new()); + + // Provide all properties explicitly + AzureLogAnalyticsAuthOptions authOptions = new("test-workspace-id", "test-dcr-id", "test-dce-endpoint"); + AzureLogAnalyticsOptions azureLogAnalyticsOptions = new(true, authOptions, "CustomLogType", 10); + TelemetryOptions telemetryOptions = new(AzureLogAnalytics: azureLogAnalyticsOptions); + config = config with { Runtime = config.Runtime with { Telemetry = telemetryOptions } }; + + // Act - Serialize to JSON + string json = config.ToJson(); + + // Assert - Should contain all properties + Assert.IsTrue(json.Contains("\"enabled\""), "Enabled should be included when explicitly provided"); + Assert.IsTrue(json.Contains("\"auth\""), "Auth options should be included"); + Assert.IsTrue(json.Contains("\"log-type\""), "Log-type should be included when explicitly provided"); + Assert.IsTrue(json.Contains("\"flush-interval-seconds\""), "Flush-interval-seconds should be included when explicitly provided"); + Assert.IsTrue(json.Contains("\"CustomLogType\""), "Custom log type value should be present"); + } +} diff --git a/src/Service.Tests/UnitTests/RuntimeConfigLoaderJsonDeserializerTests.cs b/src/Service.Tests/UnitTests/RuntimeConfigLoaderJsonDeserializerTests.cs index d933fa827d..7758c4a413 100644 --- a/src/Service.Tests/UnitTests/RuntimeConfigLoaderJsonDeserializerTests.cs +++ b/src/Service.Tests/UnitTests/RuntimeConfigLoaderJsonDeserializerTests.cs @@ -273,7 +273,7 @@ public void TestNullableOptionalProps() TryParseAndAssertOnDefaults("{" + emptyHostSubProps, out _); // Test with empty telemetry sub-properties - minJsonWithTelemetrySubProps.Append(@"{ ""application-insights"": { }, ""log-level"": { } } }"); + minJsonWithTelemetrySubProps.Append(@"{ ""application-insights"": { }, ""log-level"": { }, ""open-telemetry"": { }, ""azure-log-analytics"": { } } }"); string emptyTelemetrySubProps = minJsonWithTelemetrySubProps + "}"; TryParseAndAssertOnDefaults("{" + emptyTelemetrySubProps, out _); diff --git a/src/Service/Startup.cs b/src/Service/Startup.cs index d8cb218b75..0209848c75 100644 --- a/src/Service/Startup.cs +++ b/src/Service/Startup.cs @@ -72,6 +72,7 @@ public class Startup(IConfiguration configuration, ILogger logger) public static ApplicationInsightsOptions AppInsightsOptions = new(); public static OpenTelemetryOptions OpenTelemetryOptions = new(); + public static AzureLogAnalyticsOptions AzureLogAnalyticsOptions = new(); public const string NO_HTTPS_REDIRECT_FLAG = "--no-https-redirect"; private readonly HotReloadEventHandler _hotReloadEventHandler = new(); private RuntimeConfigProvider? _configProvider; @@ -533,6 +534,7 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env, RuntimeC // Configure Application Insights Telemetry ConfigureApplicationInsightsTelemetry(app, runtimeConfig); ConfigureOpenTelemetry(runtimeConfig); + ConfigureAzureLogAnalytics(runtimeConfig); // Config provided before starting the engine. isRuntimeReady = PerformOnConfigChangeAsync(app).Result; @@ -868,7 +870,7 @@ private void ConfigureOpenTelemetry(RuntimeConfig runtimeConfig) if (!OpenTelemetryOptions.Enabled) { - _logger.LogInformation("Open Telemetry are disabled."); + _logger.LogInformation("Open Telemetry is disabled."); return; } @@ -884,6 +886,43 @@ private void ConfigureOpenTelemetry(RuntimeConfig runtimeConfig) } } + /// + /// Configure Azure Log Analytics based on the loaded runtime configuration. If Azure Log Analytics + /// is enabled, we can track different events and metrics. + /// + /// The provider used to load runtime configuration. + /// + private void ConfigureAzureLogAnalytics(RuntimeConfig runtimeConfig) + { + if (runtimeConfig?.Runtime?.Telemetry is not null + && runtimeConfig.Runtime.Telemetry.AzureLogAnalytics is not null) + { + AzureLogAnalyticsOptions = runtimeConfig.Runtime.Telemetry.AzureLogAnalytics; + + if (!(AzureLogAnalyticsOptions.Enabled)) + { + _logger.LogInformation("Azure Log Analytics is disabled."); + return; + } + + if (AzureLogAnalyticsOptions.Auth is null) + { + _logger.LogWarning("Logs won't be sent to Azure Log Analytics because the Authorization options are not available in the runtime config."); + return; + } + + if (string.IsNullOrWhiteSpace(AzureLogAnalyticsOptions.Auth.WorkspaceId)) + { + _logger.LogWarning("Logs won't be sent to Azure Log Analytics because a Workspace Id string is not available in the runtime config."); + return; + } + + // Updating Startup Logger to Log from Startup Class. + ILoggerFactory? loggerFactory = Program.GetLoggerFactoryForLogLevel(MinimumLogLevel); + _logger = loggerFactory.CreateLogger(); + } + } + /// /// Sets Static Web Apps EasyAuth as the authentication scheme for the engine. ///