diff --git a/schemas/dab.draft.schema.json b/schemas/dab.draft.schema.json index bef83d32ac..1228b345b1 100644 --- a/schemas/dab.draft.schema.json +++ b/schemas/dab.draft.schema.json @@ -357,11 +357,44 @@ "required": [ "connection-string" ] + }, + "open-telemetry": { + "type": "object", + "additionalProperties": false, + "properties": { + "endpoint": { + "type": "string", + "description": "Open Telemetry connection string" + }, + "headers": { + "type": "string", + "description": "Open Telemetry headers" + }, + "service-name": { + "type": "string", + "description": "Open Telemetry service name", + "default": "dab" + }, + "exporter-protocol": { + "type": "string", + "description": "Open Telemetry protocol", + "default": "grpc", + "enum": [ + "grpc", + "httpprotobuf" + ] + }, + "enabled": { + "type": "boolean", + "description": "Allow enabling/disabling Open Telemetry.", + "default": true + } + }, + "required": [ + "endpoint" + ] } - }, - "required": [ - "application-insights" - ] + } } } }, diff --git a/src/Cli.Tests/AddOpenTelemetryTests.cs b/src/Cli.Tests/AddOpenTelemetryTests.cs new file mode 100644 index 0000000000..d84473980c --- /dev/null +++ b/src/Cli.Tests/AddOpenTelemetryTests.cs @@ -0,0 +1,166 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace Cli.Tests +{ + /// + /// Tests for verifying the functionality of adding OpenTelemetry to the config file. + /// + [TestClass] + public class AddOpenTelemetryTests + { + public string RUNTIME_SECTION_WITH_OPEN_TELEMETRY_SECTION = GenerateRuntimeSection(TELEMETRY_SECTION_WITH_OPEN_TELEMETRY); + public string RUNTIME_SECTION_WITH_EMPTY_TELEMETRY_SECTION = GenerateRuntimeSection(EMPTY_TELEMETRY_SECTION); + [TestInitialize] + public void TestInitialize() + { + ILoggerFactory loggerFactory = TestLoggerSupport.ProvisionLoggerFactory(); + + ConfigGenerator.SetLoggerForCliConfigGenerator(loggerFactory.CreateLogger()); + Utils.SetCliUtilsLogger(loggerFactory.CreateLogger()); + } + + /// + /// Testing to check OpenTelemetry options are correctly added to the config. + /// Verifying scenarios such as enabling/disabling OpenTelemetry and providing a valid/empty endpoint. + /// + [DataTestMethod] + [DataRow(CliBool.True, "", false, DisplayName = "Fail to add OpenTelemetry with empty endpoint.")] + [DataRow(CliBool.True, "http://localhost:4317", true, DisplayName = "Successfully adds OpenTelemetry with valid endpoint")] + [DataRow(CliBool.False, "http://localhost:4317", true, DisplayName = "Successfully adds OpenTelemetry but disabled")] + public void TestAddOpenTelemetry(CliBool isTelemetryEnabled, string endpoint, bool expectSuccess) + { + MockFileSystem fileSystem = FileSystemUtils.ProvisionMockFileSystem(); + string configPath = "test-opentelemetry-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 OpenTelemetry + bool isSuccess = ConfigGenerator.TryAddTelemetry( + new AddTelemetryOptions(openTelemetryEndpoint: endpoint, openTelemetryEnabled: isTelemetryEnabled, config: configPath), + new FileSystemRuntimeConfigLoader(fileSystem), + fileSystem); + + // Assert after adding OpenTelemetry + 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.OpenTelemetry); + Assert.AreEqual(isTelemetryEnabled is CliBool.True ? true : false, telemetryOptions.OpenTelemetry.Enabled); + Assert.AreEqual(endpoint, telemetryOptions.OpenTelemetry.Endpoint); + } + } + + /// + /// Test to verify when Telemetry section is present in the config + /// It should add OpenTelemetry if telemetry section is empty + /// or overwrite the existing OpenTelemetry with the given OpenTelemetry options. + /// + [DataTestMethod] + [DataRow(true, DisplayName = "Add OpenTelemetry when telemetry section is empty.")] + [DataRow(false, DisplayName = "Overwrite OpenTelemetry when telemetry section already exists.")] + public void TestAddOpenTelemetryWhenTelemetryAlreadyExists(bool isEmptyTelemetry) + { + MockFileSystem fileSystem = FileSystemUtils.ProvisionMockFileSystem(); + string configPath = "test-opentelemetry-config.json"; + string runtimeSection = isEmptyTelemetry ? RUNTIME_SECTION_WITH_EMPTY_TELEMETRY_SECTION : RUNTIME_SECTION_WITH_OPEN_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.OpenTelemetry); + } + else + { + Assert.IsNotNull(config.Runtime.Telemetry.OpenTelemetry); + Assert.AreEqual(true, config.Runtime.Telemetry.OpenTelemetry.Enabled); + Assert.AreEqual("http://localhost:4317", config.Runtime.Telemetry.OpenTelemetry.Endpoint); + } + + // Add OpenTelemetry + bool isSuccess = ConfigGenerator.TryAddTelemetry( + new AddTelemetryOptions(openTelemetryEndpoint: "http://localhost:4318", openTelemetryEnabled: CliBool.False, config: configPath), + new FileSystemRuntimeConfigLoader(fileSystem), + fileSystem); + + // Assert after adding OpenTelemetry + 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.OpenTelemetry); + Assert.IsFalse(config.Runtime.Telemetry.OpenTelemetry.Enabled); + Assert.AreEqual("http://localhost:4318", config.Runtime.Telemetry.OpenTelemetry.Endpoint); + } + + /// + /// 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 Application Insights enabled and a specified connection string. + /// + private const string TELEMETRY_SECTION_WITH_OPEN_TELEMETRY = @" + ""telemetry"": { + ""open-telemetry"": { + ""enabled"": true, + ""endpoint"": ""http://localhost:4317"" + } + }"; + + /// + /// Represents a JSON string for the empty telemetry section of the config. + /// + private const string EMPTY_TELEMETRY_SECTION = @" + ""telemetry"": {}"; + } +} diff --git a/src/Cli.Tests/AddTelemetryTests.cs b/src/Cli.Tests/AddTelemetryTests.cs index 4a1d878453..ac4fe99a8f 100644 --- a/src/Cli.Tests/AddTelemetryTests.cs +++ b/src/Cli.Tests/AddTelemetryTests.cs @@ -45,7 +45,7 @@ public void TestAddApplicationInsightsTelemetry(CliBool isTelemetryEnabled, stri // Add Telemetry bool isSuccess = ConfigGenerator.TryAddTelemetry( - new AddTelemetryOptions(appInsightsConnString, isTelemetryEnabled, configPath), + new AddTelemetryOptions(appInsightsConnString: appInsightsConnString, appInsightsEnabled: isTelemetryEnabled, config: configPath), new FileSystemRuntimeConfigLoader(fileSystem), fileSystem); @@ -101,7 +101,7 @@ public void TestAddAppInsightsTelemetryWhenTelemetryAlreadyExists(bool isEmptyTe // Add Telemetry bool isSuccess = ConfigGenerator.TryAddTelemetry( - new AddTelemetryOptions("InstrumentationKey=11111-1111-111-11-1", CliBool.False, configPath), + new AddTelemetryOptions(appInsightsConnString: "InstrumentationKey=11111-1111-111-11-1", appInsightsEnabled: CliBool.False, config: configPath), new FileSystemRuntimeConfigLoader(fileSystem), fileSystem); @@ -119,7 +119,7 @@ public void TestAddAppInsightsTelemetryWhenTelemetryAlreadyExists(bool isEmptyTe /// /// Generates a JSON string representing a runtime section of the config, with a customizable telemetry section. - /// + /// private static string GenerateRuntimeSection(string telemetrySection) { return $@" diff --git a/src/Cli.Tests/EndToEndTests.cs b/src/Cli.Tests/EndToEndTests.cs index 4dd3c86d07..c1dfcbf9ce 100644 --- a/src/Cli.Tests/EndToEndTests.cs +++ b/src/Cli.Tests/EndToEndTests.cs @@ -291,8 +291,8 @@ public void TestAddTelemetry(string? appInsightsEnabled, string appInsightsConnS 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); + // if --app-insights-enabled is not provided, it will default to false + Assert.AreEqual(appInsightsEnabled is null ? false : Boolean.Parse(appInsightsEnabled), updatedConfig.Runtime.Telemetry.ApplicationInsights.Enabled); Assert.AreEqual("InstrumentationKey=00000000", updatedConfig.Runtime.Telemetry.ApplicationInsights.ConnectionString); } diff --git a/src/Cli/Commands/AddTelemetryOptions.cs b/src/Cli/Commands/AddTelemetryOptions.cs index 7cd3d38be9..8ec9313d14 100644 --- a/src/Cli/Commands/AddTelemetryOptions.cs +++ b/src/Cli/Commands/AddTelemetryOptions.cs @@ -8,6 +8,7 @@ using Cli.Constants; using CommandLine; using Microsoft.Extensions.Logging; +using OpenTelemetry.Exporter; using static Cli.Utils; namespace Cli.Commands @@ -18,20 +19,54 @@ namespace Cli.Commands [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) + public AddTelemetryOptions( + string? appInsightsConnString = null, + CliBool? appInsightsEnabled = null, + string? openTelemetryEndpoint = null, + CliBool? openTelemetryEnabled = null, + string? openTelemetryHeaders = null, + OtlpExportProtocol? openTelemetryExportProtocol = null, + string? openTelemetryServiceName = null, + string? config = null) : base(config) { AppInsightsConnString = appInsightsConnString; AppInsightsEnabled = appInsightsEnabled; + OpenTelemetryEndpoint = openTelemetryEndpoint; + OpenTelemetryEnabled = openTelemetryEnabled; + OpenTelemetryHeaders = openTelemetryHeaders; + OpenTelemetryExportProtocol = openTelemetryExportProtocol; + OpenTelemetryServiceName = openTelemetryServiceName; } // 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; } + // This option is required and must be provided with a valid connection string when using app insights. + [Option("app-insights-conn-string", Required = false, 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; } + // To specify whether Application Insights telemetry should be enabled. This flag is optional and default value is false. + [Option("app-insights-enabled", Default = CliBool.False, Required = false, HelpText = "(Default: false) Enable/Disable Application Insights")] + public CliBool? AppInsightsEnabled { get; } + + // Connection string for the Open Telemetry resource to which telemetry data should be sent. + // This option is required and must be provided with a valid connection string when using open telemetry. + [Option("otel-endpoint", Required = false, HelpText = "Endpoint for Open Telemetry for telemetry data")] + public string? OpenTelemetryEndpoint { get; } + + // To specify whether Open Telemetry telemetry should be enabled. This flag is optional and default value is false. + [Option("otel-enabled", Default = CliBool.False, Required = false, HelpText = "(Default: false) Enable/Disable OTEL")] + public CliBool? OpenTelemetryEnabled { get; } + + // Headers for the Open Telemetry resource to which telemetry data should be sent. + [Option("otel-headers", Required = false, HelpText = "Headers for Open Telemetry for telemetry data")] + public string? OpenTelemetryHeaders { get; } + + // Specify the Open Telemetry protocol. This flag is optional and default value is grpc. + [Option("otel-protocol", Default = OtlpExportProtocol.Grpc, Required = false, HelpText = "(Default: grpc) Accepted: grpc/httpprotobuf")] + public OtlpExportProtocol? OpenTelemetryExportProtocol { get; } + + // Service Name for the Open Telemetry resource to which telemetry data should be sent. This flag is optional and default value is dab. + [Option("otel-service-name", Default = "dab", Required = false, HelpText = "(Default: dab) Headers for Open Telemetry for telemetry data")] + public string? OpenTelemetryServiceName { get; } public int Handler(ILogger logger, FileSystemRuntimeConfigLoader loader, IFileSystem fileSystem) { diff --git a/src/Cli/ConfigGenerator.cs b/src/Cli/ConfigGenerator.cs index e5d2ead2cf..8d4510f31a 100644 --- a/src/Cli/ConfigGenerator.cs +++ b/src/Cli/ConfigGenerator.cs @@ -619,8 +619,8 @@ private static bool TryUpdateConfiguredDataSourceOptions( /// /// Adds CosmosDB-specific options to the provided database options dictionary. - /// This method checks if the CosmosDB-specific options (database, container, and schema) are provided in the - /// configuration options. If they are, it converts their names using the provided naming policy and adds them + /// This method checks if the CosmosDB-specific options (database, container, and schema) are provided in the + /// configuration options. If they are, it converts their names using the provided naming policy and adds them /// to the database options dictionary. /// /// The dictionary to which the CosmosDB-specific options will be added. @@ -1939,17 +1939,40 @@ public static bool TryAddTelemetry(AddTelemetryOptions options, FileSystemRuntim return false; } - if (string.IsNullOrWhiteSpace(options.AppInsightsConnString)) + if (options.AppInsightsEnabled is CliBool.True && string.IsNullOrWhiteSpace(options.AppInsightsConnString)) { _logger.LogError("Invalid Application Insights connection string provided."); return false; } + if (options.OpenTelemetryEnabled is CliBool.True && string.IsNullOrWhiteSpace(options.OpenTelemetryEndpoint)) + { + _logger.LogError("Invalid OTEL endpoint provided."); + return false; + } + ApplicationInsightsOptions applicationInsightsOptions = new( Enabled: options.AppInsightsEnabled is CliBool.True ? true : false, ConnectionString: options.AppInsightsConnString ); + OpenTelemetryOptions openTelemetryOptions = new( + Enabled: options.OpenTelemetryEnabled is CliBool.True ? true : false, + Endpoint: options.OpenTelemetryEndpoint, + Headers: options.OpenTelemetryHeaders, + ExporterProtocol: options.OpenTelemetryExportProtocol, + ServiceName: options.OpenTelemetryServiceName + ); + + 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 diff --git a/src/Config/Azure.DataApiBuilder.Config.csproj b/src/Config/Azure.DataApiBuilder.Config.csproj index 223f3fee86..25dd0716f9 100644 --- a/src/Config/Azure.DataApiBuilder.Config.csproj +++ b/src/Config/Azure.DataApiBuilder.Config.csproj @@ -25,6 +25,7 @@ + diff --git a/src/Config/ObjectModel/OpenTelemetryOptions.cs b/src/Config/ObjectModel/OpenTelemetryOptions.cs new file mode 100644 index 0000000000..1f24366d9d --- /dev/null +++ b/src/Config/ObjectModel/OpenTelemetryOptions.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using OpenTelemetry.Exporter; + +namespace Azure.DataApiBuilder.Config.ObjectModel; + +/// +/// Represents the options for configuring Open Telemetry. +/// +public record OpenTelemetryOptions(bool Enabled = false, string? Endpoint = null, string? Headers = null, OtlpExportProtocol? ExporterProtocol = null, string? ServiceName = null) +{ } diff --git a/src/Config/ObjectModel/TelemetryOptions.cs b/src/Config/ObjectModel/TelemetryOptions.cs index 6d281ea2c2..5b8dae0e3d 100644 --- a/src/Config/ObjectModel/TelemetryOptions.cs +++ b/src/Config/ObjectModel/TelemetryOptions.cs @@ -6,5 +6,5 @@ namespace Azure.DataApiBuilder.Config.ObjectModel; /// /// Represents the options for telemetry. /// -public record TelemetryOptions(ApplicationInsightsOptions? ApplicationInsights) +public record TelemetryOptions(ApplicationInsightsOptions? ApplicationInsights = null, OpenTelemetryOptions? OpenTelemetry = null) { } diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index 12ce71babb..c27d838e5a 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -23,6 +23,11 @@ + + + + + diff --git a/src/Service.Tests/Configuration/OpenTelemetryTests.cs b/src/Service.Tests/Configuration/OpenTelemetryTests.cs new file mode 100644 index 0000000000..dc98c58351 --- /dev/null +++ b/src/Service.Tests/Configuration/OpenTelemetryTests.cs @@ -0,0 +1,116 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.IO; +using Azure.DataApiBuilder.Config.ObjectModel; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using OpenTelemetry.Exporter; +using OpenTelemetry.Metrics; +using OpenTelemetry.Trace; +using static Azure.DataApiBuilder.Service.Tests.Configuration.ConfigurationTests; + +namespace Azure.DataApiBuilder.Service.Tests.Configuration; + +/// +/// Contains tests for OpenTelemetry functionality. +/// +[TestClass, TestCategory(TestCategory.MSSQL)] +public class OpenTelemetryTests +{ + public TestContext TestContext { get; set; } + + private const string CONFIG_WITH_TELEMETRY = "dab-open-telemetry-test-config.json"; + private const string CONFIG_WITHOUT_TELEMETRY = "dab-no-open-telemetry-test-config.json"; + private static RuntimeConfig _configuration; + + /// + /// This is a helper function that creates runtime config file with specified telemetry options. + /// + /// Name of the config file to be created. + /// Whether telemetry is enabled or not. + /// Telemetry connection string. + public static void SetUpTelemetryInConfig(string configFileName, bool isOtelEnabled, string otelEndpoint, string otelHeaders, OtlpExportProtocol? otlpExportProtocol) + { + DataSource dataSource = new(DatabaseType.MSSQL, + GetConnectionStringFromEnvironmentConfig(environment: TestCategory.MSSQL), Options: null); + + _configuration = InitMinimalRuntimeConfig(dataSource, graphqlOptions: new(), restOptions: new()); + + TelemetryOptions _testTelemetryOptions = new(OpenTelemetry: new OpenTelemetryOptions(isOtelEnabled, otelEndpoint, otelHeaders, otlpExportProtocol, "TestServiceName")); + _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); + } + + Startup.OpenTelemetryOptions = new(); + } + + /// + /// Tests if the services are correctly enabled for Open Telemetry. + /// + [TestMethod] + public void TestOpenTelemetryServicesEnabled() + { + // Arrange + SetUpTelemetryInConfig(CONFIG_WITH_TELEMETRY, true, "http://localhost:4317", "key=key", OtlpExportProtocol.Grpc); + + string[] args = new[] + { + $"--ConfigFileName={CONFIG_WITH_TELEMETRY}" + }; + using TestServer server = new(Program.CreateWebHostBuilder(args)); + + // Additional assertions to check if OpenTelemetry is enabled correctly in services + IServiceProvider serviceProvider = server.Services; + TracerProvider tracerProvider = serviceProvider.GetService(); + MeterProvider meterProvider = serviceProvider.GetService(); + + // If tracerProvider and meterProvider are not null, OTEL is enabled + Assert.IsNotNull(tracerProvider, "TracerProvider should be registered."); + Assert.IsNotNull(meterProvider, "MeterProvider should be registered."); + } + + /// + /// Tests if the services are correctly disabled for Open Telemetry. + /// + [TestMethod] + public void TestOpenTelemetryServicesDisabled() + { + // Arrange + SetUpTelemetryInConfig(CONFIG_WITHOUT_TELEMETRY, false, null, null, null); + + string[] args = new[] + { + $"--ConfigFileName={CONFIG_WITHOUT_TELEMETRY}" + }; + using TestServer server = new(Program.CreateWebHostBuilder(args)); + + // Additional assertions to check if OpenTelemetry is disabled correctly in services + IServiceProvider serviceProvider = server.Services; + TracerProvider tracerProvider = serviceProvider.GetService(); + MeterProvider meterProvider = serviceProvider.GetService(); + + // If tracerProvider and meterProvider are null, OTEL is disabled + Assert.IsNull(tracerProvider, "TracerProvider should not be registered."); + Assert.IsNull(meterProvider, "MeterProvider should not be registered."); + } +} diff --git a/src/Service/Azure.DataApiBuilder.Service.csproj b/src/Service/Azure.DataApiBuilder.Service.csproj index a003c10282..e4fbddd825 100644 --- a/src/Service/Azure.DataApiBuilder.Service.csproj +++ b/src/Service/Azure.DataApiBuilder.Service.csproj @@ -77,7 +77,13 @@ + + + + + + @@ -95,11 +101,11 @@ - + PreserveNewest - + diff --git a/src/Service/Program.cs b/src/Service/Program.cs index 6a9e8c62df..95940414b9 100644 --- a/src/Service/Program.cs +++ b/src/Service/Program.cs @@ -14,6 +14,9 @@ using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.ApplicationInsights; +using OpenTelemetry.Exporter; +using OpenTelemetry.Logs; +using OpenTelemetry.Resources; namespace Azure.DataApiBuilder.Service { @@ -149,6 +152,22 @@ public static ILoggerFactory GetLoggerFactoryForLogLevel(LogLevel logLevel, Tele .AddFilter(category: string.Empty, logLevel); } + if (Startup.OpenTelemetryOptions.Enabled && !string.IsNullOrWhiteSpace(Startup.OpenTelemetryOptions.Endpoint)) + { + builder.AddOpenTelemetry(logging => + { + logging.IncludeFormattedMessage = true; + logging.IncludeScopes = true; + logging.SetResourceBuilder(ResourceBuilder.CreateDefault().AddService(Startup.OpenTelemetryOptions.ServiceName!)); + logging.AddOtlpExporter(configure => + { + configure.Endpoint = new Uri(Startup.OpenTelemetryOptions.Endpoint); + configure.Headers = Startup.OpenTelemetryOptions.Headers; + configure.Protocol = OtlpExportProtocol.Grpc; + }); + }); + } + builder.AddConsole(); }); } diff --git a/src/Service/Startup.cs b/src/Service/Startup.cs index cb2b301375..7350c23fe2 100644 --- a/src/Service/Startup.cs +++ b/src/Service/Startup.cs @@ -42,6 +42,10 @@ using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using OpenTelemetry.Exporter; +using OpenTelemetry.Metrics; +using OpenTelemetry.Resources; +using OpenTelemetry.Trace; using ZiggyCreatures.Caching.Fusion; using CorsOptions = Azure.DataApiBuilder.Config.ObjectModel.CorsOptions; @@ -54,6 +58,7 @@ public class Startup public static LogLevel MinimumLogLevel = LogLevel.Error; public static bool IsLogLevelOverriddenByCli; + public static OpenTelemetryOptions OpenTelemetryOptions = new(); public static ApplicationInsightsOptions AppInsightsOptions = new(); public const string NO_HTTPS_REDIRECT_FLAG = "--no-https-redirect"; @@ -92,8 +97,10 @@ public void ConfigureServices(IServiceCollection services) services.AddSingleton(configLoader); services.AddSingleton(configProvider); - if (configProvider.TryGetConfig(out RuntimeConfig? runtimeConfig) - && runtimeConfig.Runtime?.Telemetry?.ApplicationInsights is not null + bool runtimeConfigAvailable = configProvider.TryGetConfig(out RuntimeConfig? runtimeConfig); + + if (runtimeConfigAvailable + && runtimeConfig?.Runtime?.Telemetry?.ApplicationInsights is not null && runtimeConfig.Runtime.Telemetry.ApplicationInsights.Enabled) { // Add ApplicationTelemetry service and register @@ -102,6 +109,38 @@ public void ConfigureServices(IServiceCollection services) services.AddSingleton(); } + if (runtimeConfigAvailable + && runtimeConfig?.Runtime?.Telemetry?.OpenTelemetry is not null + && runtimeConfig.Runtime.Telemetry.OpenTelemetry.Enabled) + { + services.AddOpenTelemetry() + .WithMetrics(metrics => + { + metrics.SetResourceBuilder(ResourceBuilder.CreateDefault().AddService(runtimeConfig.Runtime.Telemetry.OpenTelemetry.ServiceName!)) + .AddAspNetCoreInstrumentation() + .AddHttpClientInstrumentation() + .AddOtlpExporter(configure => + { + configure.Endpoint = new Uri(runtimeConfig.Runtime.Telemetry.OpenTelemetry.Endpoint!); + configure.Headers = runtimeConfig.Runtime.Telemetry.OpenTelemetry.Headers; + configure.Protocol = OtlpExportProtocol.Grpc; + }) + .AddRuntimeInstrumentation(); + }) + .WithTracing(tracing => + { + tracing.SetResourceBuilder(ResourceBuilder.CreateDefault().AddService(runtimeConfig.Runtime.Telemetry.OpenTelemetry.ServiceName!)) + .AddAspNetCoreInstrumentation() + .AddHttpClientInstrumentation() + .AddOtlpExporter(configure => + { + configure.Endpoint = new Uri(runtimeConfig.Runtime.Telemetry.OpenTelemetry.Endpoint!); + configure.Headers = runtimeConfig.Runtime.Telemetry.OpenTelemetry.Headers; + configure.Protocol = OtlpExportProtocol.Grpc; + }); + }); + } + services.AddSingleton(implementationFactory: (serviceProvider) => { ILoggerFactory? loggerFactory = CreateLoggerFactoryForHostedAndNonHostedScenario(serviceProvider); @@ -433,7 +472,7 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env, RuntimeC } /// - /// If LogLevel is NOT overridden by CLI, attempts to find the + /// If LogLevel is NOT overridden by CLI, attempts to find the /// minimum log level based on host.mode in the runtime config if available. /// Creates a logger factory with the minimum log level. ///