Skip to content

Adding 'add-telemetry' Options to CLI for Azure Log Analytics #2772

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 41 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
ec54f52
Initial plan for issue
Copilot Jun 16, 2025
3e5a6ae
Complete: Add Azure Log Analytics properties support to DAB
Copilot Jun 16, 2025
38c272c
Fix Unit Tests
RubenCerna2079 Jun 18, 2025
967431d
Fix Unit Tests
RubenCerna2079 Jun 18, 2025
e063ab3
Changes to properties and tests
RubenCerna2079 Jun 18, 2025
982a37a
Change tests
RubenCerna2079 Jun 19, 2025
b3fb72e
Fix Unit Tests
RubenCerna2079 Jun 19, 2025
b446e75
Update AzureLogAnalyticsOptions to follow established pattern with Us…
Copilot Jun 26, 2025
bf47476
Add JsonConverterFactory for AzureLogAnalyticsOptions to use UserProv…
Copilot Jun 27, 2025
31a5d1d
Fix unit tests
RubenCerna2079 Jul 1, 2025
9f8d9b4
Changes based on comments
RubenCerna2079 Jul 2, 2025
dabfaed
Changes based on comments
RubenCerna2079 Jul 3, 2025
9fd5d75
Changes based on comments
RubenCerna2079 Jul 9, 2025
adf616a
Remove unecessary tests
RubenCerna2079 Jul 9, 2025
a2842d9
Fix Unit Tests
RubenCerna2079 Jul 9, 2025
eee31c2
Changes based on comments
RubenCerna2079 Jul 10, 2025
43cf250
Fix launch settings file
RubenCerna2079 Jul 10, 2025
5d3f0a9
Fix errors
RubenCerna2079 Jul 10, 2025
34560a3
Fix test failure
RubenCerna2079 Jul 10, 2025
f4dea9e
Changes based on comments
RubenCerna2079 Jul 15, 2025
2c4dacc
Fix Unit Tests
RubenCerna2079 Jul 15, 2025
1283d20
Initial plan for issue
Copilot Jun 16, 2025
d2baa81
Changes based on comments
RubenCerna2079 Jul 9, 2025
b7a2d85
Add CLI
RubenCerna2079 Jul 9, 2025
7faa652
Add CLI
RubenCerna2079 Jul 9, 2025
6ee43c3
Finish CLI functionality
RubenCerna2079 Jul 10, 2025
a9cad34
Add log error to CLI
RubenCerna2079 Jul 10, 2025
c3153c4
Test Refactoring
RubenCerna2079 Jul 11, 2025
b277ec3
Test Refactoring 2
RubenCerna2079 Jul 11, 2025
a93d302
Validation Changes
RubenCerna2079 Jul 14, 2025
a7147ba
Fix tests
RubenCerna2079 Jul 14, 2025
e4f506a
Fix tests
RubenCerna2079 Jul 14, 2025
792ab77
Fix changes
RubenCerna2079 Jul 21, 2025
a61be00
Add tests
RubenCerna2079 Jul 16, 2025
7ee41ed
Fix Unit Tests
RubenCerna2079 Jul 17, 2025
e24dd76
Add File with Tests
RubenCerna2079 Jul 17, 2025
aa349c8
Changes based on comments
RubenCerna2079 Jul 21, 2025
25a0deb
Fix launch settings
RubenCerna2079 Jul 21, 2025
b50261f
Comments based on changes
RubenCerna2079 Jul 21, 2025
c650698
Merge branch 'main' into dev/rubencerna/add-cli-log-analytics
RubenCerna2079 Jul 21, 2025
5c1d0dd
Merge branch 'main' into dev/rubencerna/add-cli-log-analytics
RubenCerna2079 Jul 22, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
201 changes: 201 additions & 0 deletions src/Cli.Tests/AddAzureLogAnalyticsTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

namespace Cli.Tests
{
/// <summary>
/// Tests for verifying the functionality of adding AzureLogAnalytics to the config file.
/// </summary>
[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<ConfigGenerator>());
Utils.SetCliUtilsLogger(loggerFactory.CreateLogger<Utils>());
}

/// <summary>
/// Testing to check AzureLogAnalytics options are correctly added to the config.
/// Verifying scenarios such as enabling/disabling AzureLogAnalytics and providing a valid/empty endpoint.
/// </summary>
[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);
}
}

/// <summary>
/// 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.
/// </summary>
[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);
}

/// <summary>
/// Generates a JSON string representing a runtime section of the config, with a customizable telemetry section.
/// </summary>
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"": {{}}";
}

/// <summary>
/// Represents a JSON string for the telemetry section of the config, with Azure Log Analytics enabled and specified auth properties.
/// </summary>
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""
}
}
}";

/// <summary>
/// Represents a JSON string for the empty telemetry section of the config.
/// </summary>
private const string EMPTY_TELEMETRY_SECTION = @"
""telemetry"": {}";

/// <summary>
/// Represents a JSON string for the telemetry section of the config, with Azure Log Analytics enabled and an empty auth section.
/// </summary>
private const string EMPTY_AUTH_TELEMETRY_SECTION = @"
""telemetry"": {
""azure-log-analytics"": {
""enabled"": true,
""auth"": {}
}
}";
}
}
25 changes: 25 additions & 0 deletions src/Cli.Tests/ValidateConfigTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -311,4 +311,29 @@ public async Task TestValidateAKVOptionsWithoutEndpointFails()
JsonSchemaValidationResult result = await validator.ValidateConfigSchema(config, TEST_RUNTIME_CONFIG_FILE, mockLoggerFactory.Object);
Assert.IsFalse(result.IsValid);
}

/// <summary>
/// Tests that validation fails when Azure Log Analytics options are configured without the Auth options.
/// </summary>
[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<RuntimeConfigProvider> mockRuntimeConfigProvider = new(_runtimeConfigLoader);
RuntimeConfigValidator validator = new(mockRuntimeConfigProvider.Object, _fileSystem, new Mock<ILogger<RuntimeConfigValidator>>().Object);
Mock<ILoggerFactory> mockLoggerFactory = new();
Mock<ILogger<JsonConfigSchemaValidator>> 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);
}
}
36 changes: 36 additions & 0 deletions src/Cli/Commands/AddTelemetryOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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.
Expand Down Expand Up @@ -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());
Expand Down
50 changes: 39 additions & 11 deletions src/Cli/ConfigGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 }
}
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
Loading