Skip to content

Commit

Permalink
Add support for app insights in CLI (#1952)
Browse files Browse the repository at this point in the history
## Why make this change?

- Closes #1745 
- This change will allow users to add application insights to config
through CLI

## What is this change?

- Added a new cli command `add-telemetry` to add telemetry resource
where the data will be sent. Currently DAB supports sending telemetry to
Application Insights.
- this command has two flags/options:
`--app-insights-enabled`: A boolean flag that specifies whether
Application Insights telemetry should be enabled. This flag is optional
and default value is true.
`--app-insights-conn-string`: A string that specifies the connection
string for the Application Insights resource to which telemetry data
should be sent. This flag is required must be set to a valid connection
string.

- This command will add application insights telemetry resource details
in the config, when dab engine starts with this config it will send
telemetry details to applications insights.

## How was this tested?
- [X] Unit Tests

---------

Co-authored-by: Aniruddh Munde <anmunde@microsoft.com>
Co-authored-by: Shyam Sundar J <shyamsundarj@microsoft.com>
  • Loading branch information
3 people committed Jan 25, 2024
1 parent 542ac92 commit 1f3f176
Show file tree
Hide file tree
Showing 9 changed files with 363 additions and 4 deletions.
28 changes: 28 additions & 0 deletions schemas/dab.draft.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,34 @@
}
}
}
},
"telemetry": {
"type": "object",
"description": "Telemetry configuration",
"additionalProperties": false,
"properties": {
"application-insights": {
"type": "object",
"additionalProperties": false,
"properties": {
"connection-string": {
"type": "string",
"description": "Application Insights connection string"
},
"enabled": {
"type": "boolean",
"description": "Allow enabling/disabling Application Insights telemetry.",
"default": true
}
},
"required": [
"connection-string"
]
}
},
"required": [
"application-insights"
]
}
}
},
Expand Down
169 changes: 169 additions & 0 deletions src/Cli.Tests/AddTelemetryTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

namespace Cli.Tests
{
/// <summary>
/// Tests for verifying the functionality of adding telemetry to the config file.
/// </summary>
[TestClass]
public class AddTelemetryTests
: VerifyBase
{
public string RUNTIME_SECTION_WITH_APP_INSIGHTS_TELEMETRY_SECTION = GenerateRuntimeSection(TELEMETRY_SECTION_WITH_APP_INSIGHTS);
public string RUNTIME_SECTION_WITH_EMPTY_TELEMETRY_SECTION = GenerateRuntimeSection(EMPTY_TELEMETRY_SECTION);

[TestInitialize]
public void TestInitialize()
{
ILoggerFactory loggerFactory = TestLoggerSupport.ProvisionLoggerFactory();

SetLoggerForCliConfigGenerator(loggerFactory.CreateLogger<ConfigGenerator>());
SetCliUtilsLogger(loggerFactory.CreateLogger<Utils>());
}

/// <summary>
/// Testing to check telemetry options are correctly added to the config.
/// Verifying scenarios such as enabling/disabling telemetry and providing a valid/empty connection string.
/// </summary>
[DataTestMethod]
[DataRow(CliBool.True, "", false, DisplayName = "Fail to add telemetry with empty app-insights connection string.")]
[DataRow(CliBool.True, "InstrumentationKey=00000000-0000-0000-0000-000000000000", true, DisplayName = "Successfully adds telemetry with valid connection string")]
[DataRow(CliBool.False, "InstrumentationKey=00000000-0000-0000-0000-000000000000", true, DisplayName = "Successfully adds telemetry but disabled")]
public void TestAddApplicationInsightsTelemetry(CliBool isTelemetryEnabled, string appInsightsConnString, bool expectSuccess)
{
MockFileSystem fileSystem = FileSystemUtils.ProvisionMockFileSystem();
string configPath = "test-app-insights-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 Telemetry
bool isSuccess = ConfigGenerator.TryAddTelemetry(
new AddTelemetryOptions(appInsightsConnString, isTelemetryEnabled, configPath),
new FileSystemRuntimeConfigLoader(fileSystem),
fileSystem);

// Assert after adding telemetry
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.ApplicationInsights);
Assert.AreEqual(isTelemetryEnabled is CliBool.True ? true : false, telemetryOptions.ApplicationInsights.Enabled);
Assert.AreEqual(appInsightsConnString, telemetryOptions.ApplicationInsights.ConnectionString);
}
}

/// <summary>
/// Test to verify when Telemetry section is present in the config
/// It should add application insights telemetry if telemetry section is empty
/// or overwrite the existing app insights telemetry with the given app insights telemetry options.
/// </summary>
[DataTestMethod]
[DataRow(true, DisplayName = "Add AppInsights Telemetry when telemetry section is empty.")]
[DataRow(false, DisplayName = "Overwrite AppInsights Telemetry when telemetry section already exists.")]
public void TestAddAppInsightsTelemetryWhenTelemetryAlreadyExists(bool isEmptyTelemetry)
{
MockFileSystem fileSystem = FileSystemUtils.ProvisionMockFileSystem();
string configPath = "test-app-insights-config.json";
string runtimeSection = isEmptyTelemetry ? RUNTIME_SECTION_WITH_EMPTY_TELEMETRY_SECTION : RUNTIME_SECTION_WITH_APP_INSIGHTS_TELEMETRY_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.ApplicationInsights);
}
else
{
Assert.IsNotNull(config.Runtime.Telemetry.ApplicationInsights);
Assert.AreEqual(true, config.Runtime.Telemetry.ApplicationInsights.Enabled);
Assert.AreEqual("InstrumentationKey=00000000-0000-0000-0000-000000000000", config.Runtime.Telemetry.ApplicationInsights.ConnectionString);
}

// Add Telemetry
bool isSuccess = ConfigGenerator.TryAddTelemetry(
new AddTelemetryOptions("InstrumentationKey=11111-1111-111-11-1", CliBool.False, configPath),
new FileSystemRuntimeConfigLoader(fileSystem),
fileSystem);

// Assert after adding telemetry
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.ApplicationInsights);
Assert.IsFalse(config.Runtime.Telemetry.ApplicationInsights.Enabled);
Assert.AreEqual("InstrumentationKey=11111-1111-111-11-1", config.Runtime.Telemetry.ApplicationInsights.ConnectionString);
}

/// <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 Application Insights enabled and a specified connection string.
/// </summary>
private const string TELEMETRY_SECTION_WITH_APP_INSIGHTS = @"
""telemetry"": {
""application-insights"": {
""enabled"": true,
""connection-string"": ""InstrumentationKey=00000000-0000-0000-0000-000000000000""
}
}";

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

}
43 changes: 43 additions & 0 deletions src/Cli.Tests/EndToEndTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,49 @@ public void TestAddEntity()
Assert.AreEqual(EntityActionOperation.All, entity.Permissions[0].Actions[0].Action);
}

/// <summary>
/// Test to verify telemetry details are added to the config.
/// </summary>
[DataTestMethod]
[DataRow("true", "InstrumentationKey=00000000", DisplayName = "Add Telemetry with connection string and enabled")]
[DataRow("false", "InstrumentationKey=00000000", DisplayName = "Add Telemetry with connection string and disabled")]
[DataRow(null, "InstrumentationKey=00000000", DisplayName = "Add Telemetry with connection string without enabled flag should default to enabled")]
public void TestAddTelemetry(string? appInsightsEnabled, string appInsightsConnString)
{
string[] initArgs = { "init", "-c", TEST_RUNTIME_CONFIG_FILE, "--host-mode", "development", "--database-type",
"mssql", "--connection-string", TEST_ENV_CONN_STRING };
Program.Execute(initArgs, _cliLogger!, _fileSystem!, _runtimeConfigLoader!);

Assert.IsTrue(_runtimeConfigLoader!.TryLoadConfig(TEST_RUNTIME_CONFIG_FILE, out RuntimeConfig? runtimeConfig));

// Perform assertions on various properties.
Assert.IsNotNull(runtimeConfig);
Assert.IsNotNull(runtimeConfig.Runtime);
Assert.IsNull(runtimeConfig.Runtime.Telemetry);

string[] addTelemetryArgs;
if (appInsightsEnabled is null)
{
addTelemetryArgs = new string[] { "add-telemetry", "-c", TEST_RUNTIME_CONFIG_FILE, "--app-insights-conn-string", appInsightsConnString };
}
else
{
addTelemetryArgs = new string[] { "add-telemetry", "-c", TEST_RUNTIME_CONFIG_FILE, "--app-insights-conn-string", appInsightsConnString, "--app-insights-enabled", appInsightsEnabled, };
}

Program.Execute(addTelemetryArgs, _cliLogger!, _fileSystem!, _runtimeConfigLoader!);

Assert.IsTrue(_runtimeConfigLoader!.TryLoadConfig(TEST_RUNTIME_CONFIG_FILE, out RuntimeConfig? updatedConfig));
Assert.IsNotNull(updatedConfig);
Assert.IsNotNull(updatedConfig.Runtime);
Assert.IsNotNull(updatedConfig.Runtime.Telemetry);
Assert.IsNotNull(updatedConfig.Runtime.Telemetry.ApplicationInsights);

// if --app-insights-enabled is not provided, it will default to true
Assert.AreEqual(appInsightsEnabled is null ? true : Boolean.Parse(appInsightsEnabled), updatedConfig.Runtime.Telemetry.ApplicationInsights.Enabled);
Assert.AreEqual("InstrumentationKey=00000000", updatedConfig.Runtime.Telemetry.ApplicationInsights.ConnectionString);
}

/// <summary>
/// Test to verify authentication options with init command containing
/// neither EasyAuth or Simulator as Authentication provider.
Expand Down
2 changes: 1 addition & 1 deletion src/Cli.Tests/UpdateEntityTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1127,7 +1127,7 @@ private Task ExecuteVerifyTest(string initialConfig, UpdateOptions options, Veri
{
Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(initialConfig, out RuntimeConfig? runtimeConfig), "Parsed config file.");

Assert.IsTrue(TryUpdateExistingEntity(options, runtimeConfig, out RuntimeConfig updatedRuntimeConfig), "Successfully added entity to config.");
Assert.IsTrue(TryUpdateExistingEntity(options, runtimeConfig, out RuntimeConfig updatedRuntimeConfig), "Successfully updated entity in the config.");

Assert.AreNotSame(runtimeConfig, updatedRuntimeConfig);

Expand Down
51 changes: 51 additions & 0 deletions src/Cli/Commands/AddTelemetryOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System.IO.Abstractions;
using Azure.DataApiBuilder.Config;
using Azure.DataApiBuilder.Config.ObjectModel;
using Azure.DataApiBuilder.Product;
using CommandLine;
using Microsoft.Extensions.Logging;
using static Cli.Utils;

namespace Cli.Commands
{
/// <summary>
/// Telemetry command options
/// </summary>
[Verb("add-telemetry", isDefault: false, HelpText = "Add telemetry for Data Api builder Application", Hidden = false)]
public class AddTelemetryOptions : Options
{
public AddTelemetryOptions(string appInsightsConnString, CliBool appInsightsEnabled, string? config) : base(config)
{
AppInsightsConnString = appInsightsConnString;
AppInsightsEnabled = appInsightsEnabled;
}

// Connection string for the Application Insights resource to which telemetry data should be sent.
// This option is required and must be provided with a valid connection string.
[Option("app-insights-conn-string", Required = true, HelpText = "Connection string for the Application Insights resource for telemetry data")]
public string AppInsightsConnString { get; }

// To specify whether Application Insights telemetry should be enabled. This flag is optional and default value is true.
[Option("app-insights-enabled", Default = CliBool.True, Required = false, HelpText = "(Default: true) Enable/Disable Application Insights")]
public CliBool AppInsightsEnabled { get; }

public void Handler(ILogger logger, FileSystemRuntimeConfigLoader loader, IFileSystem fileSystem)
{
logger.LogInformation("{productName} {version}", PRODUCT_NAME, ProductInfo.GetProductVersion());

bool isSuccess = ConfigGenerator.TryAddTelemetry(this, loader, fileSystem);

if (isSuccess)
{
logger.LogInformation("Successfully added telemetry to the configuration file.");
}
else
{
logger.LogError("Failed to add telemetry to the configuration file.");
}
}
}
}
48 changes: 48 additions & 0 deletions src/Cli/ConfigGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1317,5 +1317,53 @@ private static EntityGraphQLOptions ConstructUpdatedGraphQLDetails(Entity entity

return graphQLType with { Operation = graphQLOperation };
}

/// <summary>
/// This method will add the telemetry options to the config file. If the config file already has telemetry options,
/// it will overwrite the existing options.
/// Data API builder consumes the config file with provided telemetry options to send telemetry to Application Insights.
/// </summary>
public static bool TryAddTelemetry(AddTelemetryOptions options, FileSystemRuntimeConfigLoader loader, IFileSystem fileSystem)
{
if (!TryGetConfigFileBasedOnCliPrecedence(loader, options.Config, out string runtimeConfigFile))
{
return false;
}

if (!loader.TryLoadConfig(runtimeConfigFile, out RuntimeConfig? runtimeConfig))
{
_logger.LogError("Failed to read the config file: {runtimeConfigFile}.", runtimeConfigFile);
return false;
}

if (runtimeConfig.Runtime is null)
{
_logger.LogError("Invalid or missing 'runtime' section in config file: {runtimeConfigFile}.", runtimeConfigFile);
return false;
}

if (string.IsNullOrWhiteSpace(options.AppInsightsConnString))
{
_logger.LogError("Invalid Application Insights connection string provided.");
return false;
}

ApplicationInsightsOptions applicationInsightsOptions = new(
Enabled: options.AppInsightsEnabled is CliBool.True ? true : false,
ConnectionString: options.AppInsightsConnString
);

runtimeConfig = runtimeConfig with
{
Runtime = runtimeConfig.Runtime with
{
Telemetry = runtimeConfig.Runtime.Telemetry is null
? new TelemetryOptions(ApplicationInsights: applicationInsightsOptions)
: runtimeConfig.Runtime.Telemetry with { ApplicationInsights = applicationInsightsOptions }
}
};

return WriteRuntimeConfigToFile(runtimeConfigFile, runtimeConfig, fileSystem);
}
}
}
3 changes: 2 additions & 1 deletion src/Cli/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,12 +52,13 @@ public static int Execute(string[] args, ILogger cliLogger, IFileSystem fileSyst
});

// Parsing user arguments and executing required methods.
ParserResult<object>? result = parser.ParseArguments<InitOptions, AddOptions, UpdateOptions, StartOptions, ValidateOptions, ExportOptions>(args)
ParserResult<object>? result = parser.ParseArguments<InitOptions, AddOptions, UpdateOptions, StartOptions, ValidateOptions, ExportOptions, AddTelemetryOptions>(args)
.WithParsed((Action<InitOptions>)(options => options.Handler(cliLogger, loader, fileSystem)))
.WithParsed((Action<AddOptions>)(options => options.Handler(cliLogger, loader, fileSystem)))
.WithParsed((Action<UpdateOptions>)(options => options.Handler(cliLogger, loader, fileSystem)))
.WithParsed((Action<StartOptions>)(options => options.Handler(cliLogger, loader, fileSystem)))
.WithParsed((Action<ValidateOptions>)(options => options.Handler(cliLogger, loader, fileSystem)))
.WithParsed((Action<AddTelemetryOptions>)(options => options.Handler(cliLogger, loader, fileSystem)))
.WithParsed((Action<ExportOptions>)(options => Exporter.Export(options, cliLogger, loader, fileSystem)))
.WithNotParsed(err =>
{
Expand Down

0 comments on commit 1f3f176

Please sign in to comment.