diff --git a/src/Cli.Tests/AddAzureLogAnalyticsTests.cs b/src/Cli.Tests/AddAzureLogAnalyticsTests.cs new file mode 100644 index 0000000000..5af67592cb --- /dev/null +++ b/src/Cli.Tests/AddAzureLogAnalyticsTests.cs @@ -0,0 +1,201 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace Cli.Tests +{ + /// + /// Tests for verifying the functionality of adding AzureLogAnalytics to the config file. + /// + [TestClass] + public class AddAzureLogAnalyticsTests + { + public static string RUNTIME_SECTION_WITH_AZURE_LOG_ANALYTICS_SECTION = GenerateRuntimeSection(TELEMETRY_SECTION_WITH_AZURE_LOG_ANALYTICS); + public static string RUNTIME_SECTION_WITH_EMPTY_TELEMETRY_SECTION = GenerateRuntimeSection(EMPTY_TELEMETRY_SECTION); + public static string RUNTIME_SECTION_WITH_EMPTY_AUTH_SECTION = GenerateRuntimeSection(EMPTY_AUTH_TELEMETRY_SECTION); + [TestInitialize] + public void TestInitialize() + { + ILoggerFactory loggerFactory = TestLoggerSupport.ProvisionLoggerFactory(); + + ConfigGenerator.SetLoggerForCliConfigGenerator(loggerFactory.CreateLogger()); + Utils.SetCliUtilsLogger(loggerFactory.CreateLogger()); + } + + /// + /// Testing to check AzureLogAnalytics options are correctly added to the config. + /// Verifying scenarios such as enabling/disabling AzureLogAnalytics and providing a valid/empty endpoint. + /// + [DataTestMethod] + [DataRow(CliBool.True, "", "", "", false, DisplayName = "Fail to add AzureLogAnalytics with empty auth properties.")] + [DataRow(CliBool.True, "workspaceId", "dcrImmutableId", "dceEndpoint", true, DisplayName = "Successfully adds AzureLogAnalytics with valid endpoint")] + [DataRow(CliBool.False, "workspaceId", "dcrImmutableId", "dceEndpoint", true, DisplayName = "Successfully adds AzureLogAnalytics but disabled")] + public void TestAddAzureLogAnalytics(CliBool isTelemetryEnabled, string workspaceId, string dcrImmutableId, string dceEndpoint, bool expectSuccess) + { + MockFileSystem fileSystem = FileSystemUtils.ProvisionMockFileSystem(); + string configPath = "test-azureloganalytics-config.json"; + fileSystem.AddFile(configPath, new MockFileData(INITIAL_CONFIG)); + + // Initial State + Assert.IsTrue(fileSystem.FileExists(configPath)); + Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(fileSystem.File.ReadAllText(configPath), out RuntimeConfig? config)); + Assert.IsNotNull(config); + Assert.IsNotNull(config.Runtime); + Assert.IsNull(config.Runtime.Telemetry); + + // Add AzureLogAnalytics + bool isSuccess = ConfigGenerator.TryAddTelemetry( + new AddTelemetryOptions( + azureLogAnalyticsEnabled: isTelemetryEnabled, + azureLogAnalyticsWorkspaceId: workspaceId, + azureLogAnalyticsDcrImmutableId: dcrImmutableId, + azureLogAnalyticsDceEndpoint: dceEndpoint, + config: configPath), + new FileSystemRuntimeConfigLoader(fileSystem), + fileSystem); + + // Assert after adding AzureLogAnalytics + Assert.AreEqual(expectSuccess, isSuccess); + if (expectSuccess) + { + Assert.IsTrue(fileSystem.FileExists(configPath)); + Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(fileSystem.File.ReadAllText(configPath), out config)); + Assert.IsNotNull(config); + Assert.IsNotNull(config.Runtime); + Assert.IsNotNull(config.Runtime.Telemetry); + TelemetryOptions telemetryOptions = config.Runtime.Telemetry; + Assert.IsNotNull(telemetryOptions.AzureLogAnalytics); + Assert.AreEqual(isTelemetryEnabled is CliBool.True ? true : false, telemetryOptions.AzureLogAnalytics.Enabled); + Assert.IsNotNull(telemetryOptions.AzureLogAnalytics.Auth); + Assert.AreEqual(workspaceId, telemetryOptions.AzureLogAnalytics.Auth.WorkspaceId); + Assert.AreEqual(dcrImmutableId, telemetryOptions.AzureLogAnalytics.Auth.DcrImmutableId); + Assert.AreEqual(dceEndpoint, telemetryOptions.AzureLogAnalytics.Auth.DceEndpoint); + } + } + + /// + /// Test to verify when Telemetry section is present in the config + /// It should add AzureLogAnalytics if telemetry section is empty + /// or overwrite the existing AzureLogAnalytics with the given AzureLogAnalytics options. + /// + [DataTestMethod] + [DataRow(true, DisplayName = "Add AzureLogAnalytics when telemetry section is empty.")] + [DataRow(false, DisplayName = "Overwrite AzureLogAnalytics when telemetry section already exists.")] + public void TestAddAzureLogAnalyticsWhenTelemetryAlreadyExists(bool isEmptyTelemetry) + { + MockFileSystem fileSystem = FileSystemUtils.ProvisionMockFileSystem(); + string configPath = "test-azureloganalytics-config.json"; + string runtimeSection = isEmptyTelemetry ? RUNTIME_SECTION_WITH_EMPTY_TELEMETRY_SECTION : RUNTIME_SECTION_WITH_AZURE_LOG_ANALYTICS_SECTION; + string configData = $"{{{SAMPLE_SCHEMA_DATA_SOURCE},{runtimeSection}}}"; + fileSystem.AddFile(configPath, new MockFileData(configData)); + + // Initial State + Assert.IsTrue(fileSystem.FileExists(configPath)); + Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(fileSystem.File.ReadAllText(configPath), out RuntimeConfig? config)); + Assert.IsNotNull(config); + Assert.IsNotNull(config.Runtime); + Assert.IsNotNull(config.Runtime.Telemetry); + + if (isEmptyTelemetry) + { + Assert.IsNull(config.Runtime.Telemetry.AzureLogAnalytics); + } + else + { + Assert.IsNotNull(config.Runtime.Telemetry.AzureLogAnalytics); + Assert.AreEqual(true, config.Runtime.Telemetry.AzureLogAnalytics.Enabled); + Assert.IsNotNull(config.Runtime.Telemetry.AzureLogAnalytics.Auth); + Assert.AreEqual("workspaceId", config.Runtime.Telemetry.AzureLogAnalytics.Auth.WorkspaceId); + Assert.AreEqual("dcrImmutableId", config.Runtime.Telemetry.AzureLogAnalytics.Auth.DcrImmutableId); + Assert.AreEqual("dceEndpoint", config.Runtime.Telemetry.AzureLogAnalytics.Auth.DceEndpoint); + } + + // Add AzureLogAnalytics + bool isSuccess = ConfigGenerator.TryAddTelemetry( + new AddTelemetryOptions( + azureLogAnalyticsEnabled: CliBool.False, + azureLogAnalyticsWorkspaceId: "newWorkspaceId", + azureLogAnalyticsDcrImmutableId: "newDcrImmutableId", + azureLogAnalyticsDceEndpoint: "newDceEndpoint", + config: configPath), + new FileSystemRuntimeConfigLoader(fileSystem), + fileSystem); + + // Assert after adding AzureLogAnalytics + Assert.IsTrue(isSuccess); + Assert.IsTrue(fileSystem.FileExists(configPath)); + Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(fileSystem.File.ReadAllText(configPath), out config)); + Assert.IsNotNull(config); + Assert.IsNotNull(config.Runtime); + Assert.IsNotNull(config.Runtime.Telemetry); + Assert.IsNotNull(config.Runtime.Telemetry.AzureLogAnalytics); + Assert.IsFalse(config.Runtime.Telemetry.AzureLogAnalytics.Enabled); + Assert.IsNotNull(config.Runtime.Telemetry.AzureLogAnalytics.Auth); + Assert.AreEqual("newWorkspaceId", config.Runtime.Telemetry.AzureLogAnalytics.Auth.WorkspaceId); + Assert.AreEqual("newDcrImmutableId", config.Runtime.Telemetry.AzureLogAnalytics.Auth.DcrImmutableId); + Assert.AreEqual("newDceEndpoint", config.Runtime.Telemetry.AzureLogAnalytics.Auth.DceEndpoint); + } + + /// + /// Generates a JSON string representing a runtime section of the config, with a customizable telemetry section. + /// + private static string GenerateRuntimeSection(string telemetrySection) + { + return $@" + ""runtime"": {{ + ""rest"": {{ + ""path"": ""/api"", + ""enabled"": false + }}, + ""graphql"": {{ + ""path"": ""/graphql"", + ""enabled"": false, + ""allow-introspection"": true + }}, + ""host"": {{ + ""mode"": ""development"", + ""cors"": {{ + ""origins"": [], + ""allow-credentials"": false + }}, + ""authentication"": {{ + ""provider"": ""StaticWebApps"" + }} + }}, + {telemetrySection} + }}, + ""entities"": {{}}"; + } + + /// + /// Represents a JSON string for the telemetry section of the config, with Azure Log Analytics enabled and specified auth properties. + /// + private const string TELEMETRY_SECTION_WITH_AZURE_LOG_ANALYTICS = @" + ""telemetry"": { + ""azure-log-analytics"": { + ""enabled"": true, + ""auth"": { + ""workspace-id"": ""workspaceId"", + ""dcr-immutable-id"": ""dcrImmutableId"", + ""dce-endpoint"": ""dceEndpoint"" + } + } + }"; + + /// + /// Represents a JSON string for the empty telemetry section of the config. + /// + private const string EMPTY_TELEMETRY_SECTION = @" + ""telemetry"": {}"; + + /// + /// Represents a JSON string for the telemetry section of the config, with Azure Log Analytics enabled and an empty auth section. + /// + private const string EMPTY_AUTH_TELEMETRY_SECTION = @" + ""telemetry"": { + ""azure-log-analytics"": { + ""enabled"": true, + ""auth"": {} + } + }"; + } +} diff --git a/src/Cli.Tests/ValidateConfigTests.cs b/src/Cli.Tests/ValidateConfigTests.cs index 6cbc4b54f1..96c4e6df11 100644 --- a/src/Cli.Tests/ValidateConfigTests.cs +++ b/src/Cli.Tests/ValidateConfigTests.cs @@ -311,4 +311,29 @@ public async Task TestValidateAKVOptionsWithoutEndpointFails() JsonSchemaValidationResult result = await validator.ValidateConfigSchema(config, TEST_RUNTIME_CONFIG_FILE, mockLoggerFactory.Object); Assert.IsFalse(result.IsValid); } + + /// + /// Tests that validation fails when Azure Log Analytics options are configured without the Auth options. + /// + [TestMethod] + public async Task TestValidateAzureLogAnalyticsOptionsWithoutAuthFails() + { + // Arrange + string configData = $"{{{SAMPLE_SCHEMA_DATA_SOURCE},{AddAzureLogAnalyticsTests.RUNTIME_SECTION_WITH_EMPTY_AUTH_SECTION}}}"; + _fileSystem!.AddFile(TEST_RUNTIME_CONFIG_FILE, new MockFileData(configData)); + Assert.IsTrue(_fileSystem!.File.Exists(TEST_RUNTIME_CONFIG_FILE)); + Mock mockRuntimeConfigProvider = new(_runtimeConfigLoader); + RuntimeConfigValidator validator = new(mockRuntimeConfigProvider.Object, _fileSystem, new Mock>().Object); + Mock mockLoggerFactory = new(); + Mock> mockLogger = new(); + mockLoggerFactory + .Setup(factory => factory.CreateLogger(typeof(JsonConfigSchemaValidator).FullName!)) + .Returns(mockLogger.Object); + + // Assert: Settings are configured, config parses, validation fails. + string updatedConfig = _fileSystem!.File.ReadAllText(TEST_RUNTIME_CONFIG_FILE); + Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(updatedConfig, out RuntimeConfig? config)); + JsonSchemaValidationResult result = await validator.ValidateConfigSchema(config, TEST_RUNTIME_CONFIG_FILE, mockLoggerFactory.Object); + Assert.IsFalse(result.IsValid); + } } diff --git a/src/Cli/Commands/AddTelemetryOptions.cs b/src/Cli/Commands/AddTelemetryOptions.cs index 8ec9313d14..d7bab309f2 100644 --- a/src/Cli/Commands/AddTelemetryOptions.cs +++ b/src/Cli/Commands/AddTelemetryOptions.cs @@ -27,6 +27,12 @@ public AddTelemetryOptions( string? openTelemetryHeaders = null, OtlpExportProtocol? openTelemetryExportProtocol = null, string? openTelemetryServiceName = null, + CliBool? azureLogAnalyticsEnabled = null, + string? azureLogAnalyticsLogType = null, + int? azureLogAnalyticsFlushIntervalSeconds = null, + string? azureLogAnalyticsWorkspaceId = null, + string? azureLogAnalyticsDcrImmutableId = null, + string? azureLogAnalyticsDceEndpoint = null, string? config = null) : base(config) { AppInsightsConnString = appInsightsConnString; @@ -36,6 +42,12 @@ public AddTelemetryOptions( OpenTelemetryHeaders = openTelemetryHeaders; OpenTelemetryExportProtocol = openTelemetryExportProtocol; OpenTelemetryServiceName = openTelemetryServiceName; + AzureLogAnalyticsEnabled = azureLogAnalyticsEnabled; + AzureLogAnalyticsLogType = azureLogAnalyticsLogType; + AzureLogAnalyticsFlushIntervalSeconds = azureLogAnalyticsFlushIntervalSeconds; + AzureLogAnalyticsWorkspaceId = azureLogAnalyticsWorkspaceId; + AzureLogAnalyticsDcrImmutableId = azureLogAnalyticsDcrImmutableId; + AzureLogAnalyticsDceEndpoint = azureLogAnalyticsDceEndpoint; } // Connection string for the Application Insights resource to which telemetry data should be sent. @@ -68,6 +80,30 @@ public AddTelemetryOptions( [Option("otel-service-name", Default = "dab", Required = false, HelpText = "(Default: dab) Headers for Open Telemetry for telemetry data")] public string? OpenTelemetryServiceName { get; } + // To specify whether Azure Log Analytics telemetry should be enabled. This flag is optional and default value is false. + [Option("azure-log-analytics-enabled", Default = CliBool.False, Required = false, HelpText = "(Default: false) Enable/Disable Azure Log Analytics")] + public CliBool? AzureLogAnalyticsEnabled { get; } + + // Specify the table name for Azure Log Analytics resource to which telemetry data should be sent. + [Option("azure-log-analytics-log-type", Required = false, HelpText = "Log Type for Azure Log Analytics to find table to send telemetry data")] + public string? AzureLogAnalyticsLogType { get; } + + // Specify the flush interval in seconds for Azure Log Analytics resource to which telemetry data should be sent. + [Option("azure-log-analytics-flush-interval-seconds", Required = false, HelpText = "Flush Interval in seconds for Azure Log Analytics for specifying time it takes to send telemetry data")] + public int? AzureLogAnalyticsFlushIntervalSeconds { get; } + + // Specify the Workspace ID for Azure Log Analytics resource to which telemetry data should be sent. + [Option("azure-log-analytics-auth-workspace-id", Required = false, HelpText = "Workspace ID for Azure Log Analytics used to find workspace to connect")] + public string? AzureLogAnalyticsWorkspaceId { get; } + + // Specify the DCR Immutable ID for Azure Log Analytics resource to which telemetry data should be sent. + [Option("azure-log-analytics-auth-dcr-immutable-id", Required = false, HelpText = "DCR Immutable ID for Azure Log Analytics to find the data collection rule that defines how data is collected")] + public string? AzureLogAnalyticsDcrImmutableId { get; } + + // Specify the DCE Endpoint for Azure Log Analytics resource to which telemetry data should be sent. + [Option("azure-log-analytics-auth-dce-endpoint", Required = false, HelpText = "DCE Endpoint for Azure Log Analytics to find table to send telemetry data")] + public string? AzureLogAnalyticsDceEndpoint { get; } + public int Handler(ILogger logger, FileSystemRuntimeConfigLoader loader, IFileSystem fileSystem) { logger.LogInformation("{productName} {version}", PRODUCT_NAME, ProductInfo.GetProductVersion()); diff --git a/src/Cli/ConfigGenerator.cs b/src/Cli/ConfigGenerator.cs index ced4649590..3636bc1f00 100644 --- a/src/Cli/ConfigGenerator.cs +++ b/src/Cli/ConfigGenerator.cs @@ -1961,6 +1961,33 @@ public static bool TryAddTelemetry(AddTelemetryOptions options, FileSystemRuntim return false; } + if (options.AzureLogAnalyticsEnabled is CliBool.True) + { + bool returnError = false; + if (string.IsNullOrWhiteSpace(options.AzureLogAnalyticsWorkspaceId)) + { + _logger.LogError("Invalid Azure Log Analytics workspace-id provided"); + returnError = true; + } + + if (string.IsNullOrWhiteSpace(options.AzureLogAnalyticsDcrImmutableId)) + { + _logger.LogError("Invalid Azure Log Analytics dcr-immutable-id provided"); + returnError = true; + } + + if (string.IsNullOrWhiteSpace(options.AzureLogAnalyticsDceEndpoint)) + { + _logger.LogError("Invalid Azure Log Analytics dce-endpoint provided"); + returnError = true; + } + + if (returnError) + { + return false; + } + } + ApplicationInsightsOptions applicationInsightsOptions = new( Enabled: options.AppInsightsEnabled is CliBool.True ? true : false, ConnectionString: options.AppInsightsConnString @@ -1974,22 +2001,23 @@ public static bool TryAddTelemetry(AddTelemetryOptions options, FileSystemRuntim ServiceName: options.OpenTelemetryServiceName ); + AzureLogAnalyticsOptions azureLogAnalyticsOptions = new( + enabled: options.AzureLogAnalyticsEnabled is CliBool.True ? true : false, + auth: new( + workspaceId: options.AzureLogAnalyticsWorkspaceId, + dcrImmutableId: options.AzureLogAnalyticsDcrImmutableId, + dceEndpoint: options.AzureLogAnalyticsDceEndpoint), + logType: options.AzureLogAnalyticsLogType, + flushIntervalSeconds: options.AzureLogAnalyticsFlushIntervalSeconds + ); + runtimeConfig = runtimeConfig with { Runtime = runtimeConfig.Runtime with { Telemetry = runtimeConfig.Runtime.Telemetry is null - ? new TelemetryOptions(ApplicationInsights: applicationInsightsOptions, OpenTelemetry: openTelemetryOptions) - : runtimeConfig.Runtime.Telemetry with { ApplicationInsights = applicationInsightsOptions, OpenTelemetry = openTelemetryOptions } - } - }; - runtimeConfig = runtimeConfig with - { - Runtime = runtimeConfig.Runtime with - { - Telemetry = runtimeConfig.Runtime.Telemetry is null - ? new TelemetryOptions(ApplicationInsights: applicationInsightsOptions) - : runtimeConfig.Runtime.Telemetry with { ApplicationInsights = applicationInsightsOptions } + ? new TelemetryOptions(ApplicationInsights: applicationInsightsOptions, OpenTelemetry: openTelemetryOptions, AzureLogAnalytics: azureLogAnalyticsOptions) + : runtimeConfig.Runtime.Telemetry with { ApplicationInsights = applicationInsightsOptions, OpenTelemetry = openTelemetryOptions, AzureLogAnalytics = azureLogAnalyticsOptions } } }; diff --git a/src/Config/Converters/AzureLogAnalyticsAuthOptionsConverter.cs b/src/Config/Converters/AzureLogAnalyticsAuthOptionsConverter.cs index 29f30c8b95..1d790b125d 100644 --- a/src/Config/Converters/AzureLogAnalyticsAuthOptionsConverter.cs +++ b/src/Config/Converters/AzureLogAnalyticsAuthOptionsConverter.cs @@ -73,7 +73,6 @@ public AzureLogAnalyticsAuthOptionsConverter(bool replaceEnvVar) throw new JsonException($"Unexpected property {propertyName}"); } } - } throw new JsonException("Failed to read the Azure Log Analytics Auth Options"); diff --git a/src/Config/Converters/AzureLogAnalyticsOptionsConverterFactory.cs b/src/Config/Converters/AzureLogAnalyticsOptionsConverterFactory.cs index c327796ad6..0121cb73f8 100644 --- a/src/Config/Converters/AzureLogAnalyticsOptionsConverterFactory.cs +++ b/src/Config/Converters/AzureLogAnalyticsOptionsConverterFactory.cs @@ -141,7 +141,7 @@ public override void Write(Utf8JsonWriter writer, AzureLogAnalyticsOptions value JsonSerializer.Serialize(writer, value.Enabled, options); } - if (value?.Auth is not null) + if (value?.Auth is not null && (value.Auth.UserProvidedWorkspaceId || value.Auth.UserProvidedDcrImmutableId || value.Auth.UserProvidedDceEndpoint)) { AzureLogAnalyticsAuthOptionsConverter authOptionsConverter = options.GetConverter(typeof(AzureLogAnalyticsAuthOptions)) as AzureLogAnalyticsAuthOptionsConverter ?? throw new JsonException("Failed to get azure-log-analytics.auth options converter");