diff --git a/docs/configuration.asciidoc b/docs/configuration.asciidoc new file mode 100644 index 0000000..eff42f2 --- /dev/null +++ b/docs/configuration.asciidoc @@ -0,0 +1,197 @@ +[[configuration]] +== Configuration + +Configuration of the OpenTelemetry SDK should be performed through the +mechanisms https://opentelemetry.io/docs/languages/net/automatic/configuration/[documented on the OpenTelemetry website]. + +The Elastic distribution can be further configured using advanced settings when +you need complete control of its behaviour. Configuration can be achieved by setting environment variables, +using the `IConfiguration` integration, or manually configuring the Elastic distribution. + +=== Environment variables + +The Elastic distribution can be configured using environment variables. This is a cross-platform +way to configure the Elastic distribution and is especially useful in containerized environments. + +Environment variables are read at startup and can be used to configure the Elastic distribution. +For details of the various options available and their corresponding environment variable names, +see <>. + +Environment variables always take precedence over configuration provided by the `IConfiguration` +system. + +=== IConfiguration integration + +In applications that use the "host" pattern, such as ASP.NET Core and worker service, the Elastic +distribution can be configured using the `IConfiguration` integration. This is done by passing an +`IConfiguration` instance to the `AddElasticOpenTelemetry` extension method on the `IServiceCollection`. + +When using an `IHostApplicationBuilder` such as modern ASP.NET Core applications, the current `IConfiguration` +can be accessed via the `Configuration` property on the builder. + +[source,csharp] +---- +var builder = WebApplication.CreateBuilder(args); +var currentConfig = builder.Configuration; <1> +---- +<1> Access the current `IConfiguration` instance from the builder. + +By default, at this stage, the configuration will be populated from the default configuration sources, +including the `appsettings.json` file(s) and command-line arguments. You may use these sources to define +the configuration for the Elastic distribution. + +For example, you can define the configuration for the Elastic distribution in the `appsettings.json` file: + +[source,json] +---- +{ + "Elastic": { + "OpenTelemetry": { + "FileLogDirectory": "C:\\Logs" <1> + } + } +} +---- +<1> This example sets the file log directory to `C:\Logs` which enables diagnostic file logging. + +Configuration from the "Elastic:OpenTelemetry" section of the `IConfiguration` instance will be +bound to the `ElasticOpenTelemetryOptions` instance used to configure the Elastic distribution. + +To learn more about the Microsoft configuration system, see +https://learn.microsoft.com/en-us/aspnet/core/fundamentals/configuration[Configuration in ASP.NET Core]. + +=== Manual configuration + +In all other scenarios, configuration can be achieved manually in code. This is done by creating +an instance of `ElasticOpenTelemetryBuilderOptions` and passing it to the `ElasticOpenTelemetryBuilder` constructor +or an overload of the `AddElasticOpenTelemetry` extension method on the `IServiceCollection`. + +For example, in traditional console applications, you can configure the Elastic distribution like this: + +[source,csharp] +---- +using Elastic.OpenTelemetry; +using Elastic.OpenTelemetry.Configuration; +using Elastic.OpenTelemetry.Extensions; +using Microsoft.Extensions.DependencyInjection; +using OpenTelemetry; + +var services = new ServiceCollection(); + +var builderOptions = new ElasticOpenTelemetryBuilderOptions <1> +{ + DistroOptions = new ElasticOpenTelemetryOptions <2> + { + FileLogDirectory = "C:\\Logs", <3> + } +}; + +await using var session = new ElasticOpenTelemetryBuilder(builderOptions) <4> + .WithTracing(b => b.AddSource("MySource")) + .Build(); +---- +<1> Create an instance of `ElasticOpenTelemetryBuilderOptions` +<2> Create an instance of `ElasticOpenTelemetryOptions` and configure the file log directory by +setting the corresponding property. +<3> This example sets the file log directory to `C:\Logs` which enables diagnostic file logging. +<4> Pass the `ElasticOpenTelemetryBuilderOptions` instance to the `ElasticOpenTelemetryBuilder` constructor +to configure the Elastic distribution. + +[[configuration-options]] +=== Configuration options + +[float] +[[config-filelogdirectory]] +==== `FileLogDirectory` + +A string specifying the directory where the Elastic distribution will write diagnostic log files. +When not provided, no file logging will occur. Each new .NET process will create a new log file in the +specified directory. + +[options="header"] +|============ +| Environment variable name | IConfiguration key +| `ELASTIC_OTEL_FILE_LOG_DIRECTORY` | `Elastic:OpenTelemetry:FileLogDirectory` +|============ + +[options="header"] +|============ +| Default | Type +| `string.Empty` | String +|============ + + +[float] +[[config-fileloglevel]] +==== `FileLogLevel` + +Sets the logging level for the distribtuion. + +Valid options: `Critical`, `Error`, `Warning`, `Information`, `Debug`, `Trace` and `None` (`None` disables the logging). + +[options="header"] +|============ +| Environment variable name | IConfiguration key +| `ELASTIC_OTEL_FILE_LOG_LEVEL` | `Elastic:OpenTelemetry:FileLogLevel` +|============ + +[options="header"] +|============ +| Default | Type +| `Information` | String +|============ + + +[float] +[[config-skipotlpexporter]] +==== `SkipOtlpExporter` + +Allows the distribution to used with its defaults, but without enabling the export of telemetry data to +an OTLP endpoint. This can be useful when you want to test applications without sending telemetry data. + +[options="header"] +|============ +| Environment variable name | IConfiguration key +| `ELASTIC_OTEL_SKIP_OTLP_EXPORTER` | `Elastic:OpenTelemetry:SkipOtlpExporter` +|============ + +[options="header"] +|============ +| Default | Type +| `false` | Bool +|============ + + +[float] +[[config-enabledelasticdefaults]] +==== `EnabledElasticDefaults` + +A comma-separated list of Elastic defaults to enable. This can be useful when you want to enable +only some of the Elastic distribution opinionated defaults. + +Valid options: `None`, `Tracing`, `Metrics`, `Logging`. + +Except for the `None` option, all other options can be combined. + +When this setting is not configured or the value is `string.Empty`, all Elastic distribution defaults will be enabled. + +When `None` is specified, no Elastic distribution defaults will be enabled, and you will need to manually +configure the OpenTelemetry SDK to enable collection of telemetry signals. In this mode, the Elastic distribution +does not provide any opinionated defaults, nor register any processors, allowing you to start with the "vanilla" +OpenTelemetry SDK configuration. You may then choose to configure the various providers and register processors +as required. + +In all other cases, the Elastic distribution will enable the specified defaults. For example, to enable only +Elastic defaults only for tracing and metrics, set this value to `Tracing,Metrics`. + +[options="header"] +|============ +| Environment variable name | IConfiguration key +| `ELASTIC_OTEL_ENABLE_ELASTIC_DEFAULTS` | `Elastic:OpenTelemetry:EnabledElasticDefaults` +|============ + +[options="header"] +|============ +| Default | Type +| `string.Empty` | String +|============ \ No newline at end of file diff --git a/docs/index.asciidoc b/docs/index.asciidoc index 76e0822..fb5e068 100644 --- a/docs/index.asciidoc +++ b/docs/index.asciidoc @@ -6,3 +6,5 @@ include::{asciidoc-dir}/../../shared/attributes.asciidoc[] include::./intro.asciidoc[] include::./getting-started.asciidoc[] + +include::./configuration.asciidoc[] \ No newline at end of file diff --git a/examples/Example.MinimalApi/Program.cs b/examples/Example.MinimalApi/Program.cs index 5ddc273..7489ac0 100644 --- a/examples/Example.MinimalApi/Program.cs +++ b/examples/Example.MinimalApi/Program.cs @@ -8,11 +8,12 @@ var builder = WebApplication.CreateBuilder(args); +// This will add the OpenTelemetry services using Elastic defaults builder.AddServiceDefaults(); builder.Services .AddHttpClient() // Adds IHttpClientFactory - .AddOpenTelemetry() // Adds the OpenTelemetry SDK + .AddOpenTelemetry() // Adds app specific tracing .WithTracing(t => t.AddSource(Api.ActivitySourceName)); var app = builder.Build(); diff --git a/examples/Example.MinimalApi/appsettings.json b/examples/Example.MinimalApi/appsettings.json index 393893c..c058ae2 100644 --- a/examples/Example.MinimalApi/appsettings.json +++ b/examples/Example.MinimalApi/appsettings.json @@ -3,11 +3,17 @@ "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning", - "Elastic.OpenTelemetry": "Information" + "Elastic.OpenTelemetry": "Warning" } }, "AllowedHosts": "*", "AspNetCoreInstrumentation": { "RecordException": "true" + }, + "Elastic": { + "OpenTelemetry": { + "FileLogDirectory": "C:\\Logs\\OtelDistro", + "FileLogLevel": "Information" + } } } diff --git a/examples/ServiceDefaults/Extensions.cs b/examples/ServiceDefaults/Extensions.cs index 141070c..601c411 100644 --- a/examples/ServiceDefaults/Extensions.cs +++ b/examples/ServiceDefaults/Extensions.cs @@ -44,7 +44,7 @@ public static IHostApplicationBuilder ConfigureOpenTelemetry(this IHostApplicati logging.IncludeScopes = true; }); - builder.Services.AddOpenTelemetry() + builder.Services.AddElasticOpenTelemetry(builder.Configuration) .WithMetrics(metrics => { metrics.AddAspNetCoreInstrumentation() diff --git a/src/Elastic.OpenTelemetry/ElasticOpenTelemetryOptions.cs b/src/Elastic.OpenTelemetry/Configuration/ElasticOpenTelemetryBuilderOptions.cs similarity index 66% rename from src/Elastic.OpenTelemetry/ElasticOpenTelemetryOptions.cs rename to src/Elastic.OpenTelemetry/Configuration/ElasticOpenTelemetryBuilderOptions.cs index 470fcb7..81b5e94 100644 --- a/src/Elastic.OpenTelemetry/ElasticOpenTelemetryOptions.cs +++ b/src/Elastic.OpenTelemetry/Configuration/ElasticOpenTelemetryBuilderOptions.cs @@ -5,13 +5,15 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -namespace Elastic.OpenTelemetry; +namespace Elastic.OpenTelemetry.Configuration; /// /// Expert options to provide to to control its initial OpenTelemetry registration. /// -public record ElasticOpenTelemetryOptions +public record ElasticOpenTelemetryBuilderOptions { + private ElasticOpenTelemetryOptions? _elasticOpenTelemetryOptions; + /// /// Provide an additional logger to the internal file logger. /// @@ -24,15 +26,15 @@ public record ElasticOpenTelemetryOptions /// Provides an to register the into. /// If null, a new local instance will be used. /// - public IServiceCollection? Services { get; init; } - - /// - /// Stops from registering OLTP exporters, useful for testing scenarios. - /// - public bool SkipOtlpExporter { get; init; } + internal IServiceCollection? Services { get; init; } /// - /// Optional name which is used when retrieving OTLP options. + /// Advanced options which can be used to finely-tune the behaviour of the Elastic + /// distribution of OpenTelemetry. /// - public string? OtlpExporterName { get; init; } + public ElasticOpenTelemetryOptions DistroOptions + { + get => _elasticOpenTelemetryOptions ?? new(); + init => _elasticOpenTelemetryOptions = value; + } } diff --git a/src/Elastic.OpenTelemetry/Configuration/ElasticOpenTelemetryOptions.cs b/src/Elastic.OpenTelemetry/Configuration/ElasticOpenTelemetryOptions.cs new file mode 100644 index 0000000..5f9a2ef --- /dev/null +++ b/src/Elastic.OpenTelemetry/Configuration/ElasticOpenTelemetryOptions.cs @@ -0,0 +1,257 @@ +// Licensed to Elasticsearch B.V under one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; + +namespace Elastic.OpenTelemetry.Configuration; + +/// +/// Defines advanced options which can be used to finely-tune the behaviour of the Elastic +/// distribution of OpenTelemetry. +/// +/// +/// Options are bound from the following sources: +/// +/// Environment variables +/// An instance +/// +/// Options initialised via property initializers take precedence over bound values. +/// Environment variables take precedence over values. +/// +public class ElasticOpenTelemetryOptions +{ + private static readonly string ConfigurationSection = "Elastic:OpenTelemetry"; + private static readonly string FileLogDirectoryConfigPropertyName = "FileLogDirectory"; + private static readonly string FileLogLevelConfigPropertyName = "FileLogLevel"; + private static readonly string SkipOtlpExporterConfigPropertyName = "SkipOtlpExporter"; + private static readonly string EnabledElasticDefaultsConfigPropertyName = "EnabledElasticDefaults"; + + // For a relatively limited number of properties, this is okay. If this grows significantly, consider a + // more flexible approach similar to the layered configuration used in the Elastic APM Agent. + private EnabledElasticDefaults? _elasticDefaults; + private string? _fileLogDirectory; + private ConfigSource _fileLogDirectorySource = ConfigSource.Default; + private string? _fileLogLevel; + private ConfigSource _fileLogLevelSource = ConfigSource.Default; + private bool? _skipOtlpExporter; + private ConfigSource _skipOtlpExporterSource = ConfigSource.Default; + private string? _enabledElasticDefaults; + private ConfigSource _enabledElasticDefaultsSource = ConfigSource.Default; + + /// + /// Creates a new instance of with properties + /// bound from environment variables. + /// + public ElasticOpenTelemetryOptions() + { + SetFromEnvironment(EnvironmentVariables.ElasticOtelFileLogDirectoryEnvironmentVariable, ref _fileLogDirectory, + ref _fileLogDirectorySource, StringParser); + SetFromEnvironment(EnvironmentVariables.ElasticOtelFileLogLevelEnvironmentVariable, ref _fileLogLevel, + ref _fileLogLevelSource, StringParser); + SetFromEnvironment(EnvironmentVariables.ElasticOtelSkipOtlpExporter, ref _skipOtlpExporter, + ref _skipOtlpExporterSource, BoolParser); + SetFromEnvironment(EnvironmentVariables.ElasticOtelEnableElasticDefaults, ref _enabledElasticDefaults, + ref _enabledElasticDefaultsSource, StringParser); + } + + /// + /// Creates a new instance of with properties + /// bound from environment variables and an instance. + /// + internal ElasticOpenTelemetryOptions(IConfiguration configuration) : this() + { + SetFromConfiguration(configuration, FileLogDirectoryConfigPropertyName, ref _fileLogDirectory, + ref _fileLogDirectorySource, StringParser); + SetFromConfiguration(configuration, FileLogLevelConfigPropertyName, ref _fileLogLevel, + ref _fileLogLevelSource, StringParser); + SetFromConfiguration(configuration, SkipOtlpExporterConfigPropertyName, ref _skipOtlpExporter, + ref _skipOtlpExporterSource, BoolParser); + SetFromConfiguration(configuration, EnabledElasticDefaultsConfigPropertyName, ref _enabledElasticDefaults, + ref _enabledElasticDefaultsSource, StringParser); + } + + /// + /// The output directory where the Elastic distribution of OpenTelemetry will write log files. + /// + /// + /// When configured, a file log will be created in this directory with the name + /// {ProcessName}_{UtcUnixTimeMilliseconds}_{ProcessId}.instrumentation.log. + /// This log file includes log messages from the OpenTelemetry SDK and the Elastic distribution. + /// + public string FileLogDirectory + { + get => _fileLogDirectory ?? string.Empty; + init + { + _fileLogDirectory = value; + _fileLogDirectorySource = ConfigSource.Property; + } + } + + /// + /// The log level to use when writing log files. + /// + /// + /// Valid values are: + /// + /// NoneDisables logging. + /// CriticalFailures that require immediate attention. + /// ErrorErrors and exceptions that cannot be handled. + /// WarningAbnormal or unexpected events. + /// InformationGeneral information about the distribution and OpenTelemetry SDK. + /// DebugRich debugging and development. + /// TraceContain the most detailed messages. + /// + /// + public string FileLogLevel + { + get => _fileLogLevel ?? "Information"; + init + { + _fileLogLevel = value; + _fileLogLevelSource = ConfigSource.Property; + } + } + + /// + /// Stops from registering OLTP exporters, useful for testing scenarios. + /// + public bool SkipOtlpExporter + { + get => _skipOtlpExporter ?? false; + init + { + _skipOtlpExporter = value; + _skipOtlpExporterSource = ConfigSource.Property; + } + } + + /// + /// A comma separated list of instrumentation signal Elastic defaults. + /// + /// + /// Valid values are: + /// + /// NoneDisables all Elastic defaults resulting in the use of the "vanilla" SDK. + /// AllEnables all defaults (default if this option is not specified). + /// TracingEnables Elastic defaults for tracing. + /// MetricsEnables Elastic defaults for metrics. + /// LoggingEnables Elastic defaults for logging. + /// + /// + public string EnableElasticDefaults + { + get => _enabledElasticDefaults ?? string.Empty; + init + { + _enabledElasticDefaults = value; + _enabledElasticDefaultsSource = ConfigSource.Property; + } + } + + internal EnabledElasticDefaults EnabledDefaults => _elasticDefaults ?? GetEnabledElasticDefaults(); + + private static (bool, string) StringParser(string? s) => !string.IsNullOrEmpty(s) ? (true, s) : (false, string.Empty); + + private static (bool, bool?) BoolParser(string? s) => bool.TryParse(s, out var boolValue) ? (true, boolValue) : (false, null); + + private static void SetFromEnvironment(string key, ref T field, ref ConfigSource configSourceField, Func parser) + { + var (success, value) = parser(Environment.GetEnvironmentVariable(key)); + + if (success) + { + field = value; + configSourceField = ConfigSource.Environment; + } + } + + private static void SetFromConfiguration(IConfiguration configuration, string key, ref T field, ref ConfigSource configSourceField, + Func parser) + { + if (field is null) + { + var logFileDirectory = configuration?.GetValue($"{ConfigurationSection}:{key}"); + + var (success, value) = parser(logFileDirectory); + + if (success) + { + field = value; + configSourceField = ConfigSource.IConfiguration; + } + } + } + + private EnabledElasticDefaults GetEnabledElasticDefaults() + { + if (_elasticDefaults.HasValue) + return _elasticDefaults.Value; + + var defaults = EnabledElasticDefaults.None; + + // NOTE: Using spans is an option here, but it's quite complex and this should only ever happen once per process + + if (string.IsNullOrEmpty(EnableElasticDefaults)) + return All(); + + var elements = EnableElasticDefaults.Split(',', StringSplitOptions.RemoveEmptyEntries); + + if (elements.Length == 1 && elements[0].Equals("None", StringComparison.OrdinalIgnoreCase)) + return EnabledElasticDefaults.None; + + foreach (var element in elements) + { + if (element.Equals("Tracing", StringComparison.OrdinalIgnoreCase)) + defaults |= EnabledElasticDefaults.Tracing; + else if (element.Equals("Metrics", StringComparison.OrdinalIgnoreCase)) + defaults |= EnabledElasticDefaults.Metrics; + else if (element.Equals("Logging", StringComparison.OrdinalIgnoreCase)) + defaults |= EnabledElasticDefaults.Logging; + } + + // If we get this far without any matched elements, default to all + if (defaults.Equals(EnabledElasticDefaults.None)) + defaults = All(); + + _elasticDefaults = defaults; + + return defaults; + + static EnabledElasticDefaults All() => EnabledElasticDefaults.Tracing | EnabledElasticDefaults.Metrics | EnabledElasticDefaults.Logging; + } + + internal void LogConfigSources(ILogger logger) + { + logger.LogInformation("Configured value for {ConfigKey}: '{ConfigValue}' from [{ConfigSource}]", FileLogDirectoryConfigPropertyName, + _fileLogDirectory, _fileLogDirectorySource); + + logger.LogInformation("Configured value for {ConfigKey}: '{ConfigValue}' from [{ConfigSource}]", FileLogLevelConfigPropertyName, + _fileLogLevel, _fileLogLevelSource); + + logger.LogInformation("Configured value for {ConfigKey}: '{ConfigValue}' from [{ConfigSource}]", SkipOtlpExporterConfigPropertyName, + _skipOtlpExporter, _skipOtlpExporterSource); + + logger.LogInformation("Configured value for {ConfigKey}: '{ConfigValue}' from [{ConfigSource}]", EnabledElasticDefaultsConfigPropertyName, + _enabledElasticDefaults, _enabledElasticDefaultsSource); + } + + [Flags] + internal enum EnabledElasticDefaults + { + None, + Tracing = 1 << 0, //1 + Metrics = 1 << 1, //2 + Logging = 1 << 2, //4 + } + + private enum ConfigSource + { + Default, // Default value assigned within this class + Environment, // Loaded from an environment variable + IConfiguration, // Bound from an IConfiguration instance + Property // Set via property initializer + } +} diff --git a/src/Elastic.OpenTelemetry/Configuration/EnvironmentVariables.cs b/src/Elastic.OpenTelemetry/Configuration/EnvironmentVariables.cs new file mode 100644 index 0000000..d578d88 --- /dev/null +++ b/src/Elastic.OpenTelemetry/Configuration/EnvironmentVariables.cs @@ -0,0 +1,13 @@ +// Licensed to Elasticsearch B.V under one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +namespace Elastic.OpenTelemetry.Configuration; + +internal static class EnvironmentVariables +{ + public const string ElasticOtelSkipOtlpExporter = "ELASTIC_OTEL_SKIP_OTLP_EXPORTER"; + public const string ElasticOtelFileLogDirectoryEnvironmentVariable = "ELASTIC_OTEL_FILE_LOG_DIRECTORY"; + public const string ElasticOtelFileLogLevelEnvironmentVariable = "ELASTIC_OTEL_FILE_LOG_LEVEL"; + public const string ElasticOtelEnableElasticDefaults = "ELASTIC_OTEL_ENABLE_ELASTIC_DEFAULTS"; +} diff --git a/src/Elastic.OpenTelemetry/DependencyInjection/OpenTelemetryServicesExtensions.cs b/src/Elastic.OpenTelemetry/DependencyInjection/OpenTelemetryServicesExtensions.cs index fde7dff..b76a403 100644 --- a/src/Elastic.OpenTelemetry/DependencyInjection/OpenTelemetryServicesExtensions.cs +++ b/src/Elastic.OpenTelemetry/DependencyInjection/OpenTelemetryServicesExtensions.cs @@ -2,7 +2,6 @@ // Elasticsearch B.V licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information -using Elastic.OpenTelemetry; using Microsoft.Extensions.DependencyInjection; /// @@ -12,7 +11,7 @@ public static class OpenTelemetryServicesExtensions { // ReSharper disable RedundantNameQualifier - +#pragma warning disable IDE0001 /// /// /// Uses defaults particularly well suited for Elastic's Observability offering because Elastic.OpenTelemetry is referenced @@ -28,27 +27,6 @@ public static class OpenTelemetryServicesExtensions this global::Microsoft.Extensions.DependencyInjection.IServiceCollection services ) => services.AddElasticOpenTelemetry(); - /// - /// - /// Uses defaults particularly well suited for Elastic's Observability offering because Elastic.OpenTelemetry is referenced - /// - /// - /// - /// - /// - /// Expert level options to control the bootstrapping of the Elastic Agent - /// - /// - /// - public static global::OpenTelemetry.IOpenTelemetryBuilder AddOpenTelemetry( - this global::Microsoft.Extensions.DependencyInjection.IServiceCollection services - , ElasticOpenTelemetryOptions options - ) - { - if (options.Services == null) - options = options with { Services = services }; - return services.AddElasticOpenTelemetry(options); - } - // ReSharper enable RedundantNameQualifier +#pragma warning restore IDE0001 } diff --git a/src/Elastic.OpenTelemetry/DependencyInjection/ServiceCollectionExtensions.cs b/src/Elastic.OpenTelemetry/DependencyInjection/ServiceCollectionExtensions.cs index c4382d7..6ca0388 100644 --- a/src/Elastic.OpenTelemetry/DependencyInjection/ServiceCollectionExtensions.cs +++ b/src/Elastic.OpenTelemetry/DependencyInjection/ServiceCollectionExtensions.cs @@ -3,6 +3,8 @@ // See the LICENSE file in the project root for more information using Elastic.OpenTelemetry; +using Elastic.OpenTelemetry.Configuration; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using OpenTelemetry; @@ -17,26 +19,44 @@ public static class ServiceCollectionExtensions /// /// Registers the Elastic OpenTelemetry builder with the provided . /// - /// The for adding services. + /// The for adding services. /// /// An instance of that can be used to further configure the /// OpenTelemetry SDK. /// - public static IOpenTelemetryBuilder AddElasticOpenTelemetry(this IServiceCollection serviceCollection) => - serviceCollection.AddElasticOpenTelemetry(new ElasticOpenTelemetryOptions { Services = serviceCollection }); + public static IOpenTelemetryBuilder AddElasticOpenTelemetry(this IServiceCollection services) => + services.AddElasticOpenTelemetry(new ElasticOpenTelemetryBuilderOptions { Services = services }); + + /// + /// Registers the Elastic OpenTelemetry builder with the provided . + /// + /// The for adding services. + /// + /// An instance from which to attempt binding of configuration values. + /// + /// + /// An instance of that can be used to further configure the + /// OpenTelemetry SDK. + /// + public static IOpenTelemetryBuilder AddElasticOpenTelemetry(this IServiceCollection services, IConfiguration configuration) => + services.AddElasticOpenTelemetry(new ElasticOpenTelemetryBuilderOptions + { + Services = services, + DistroOptions = new ElasticOpenTelemetryOptions(configuration) + }); /// /// Registers the Elastic OpenTelemetry builder with the provided . /// - /// The for adding services. - /// for the initial OpenTelemetry registration. + /// The for adding services. + /// for the initial OpenTelemetry registration. /// /// An instance of that can be used to further configure the /// OpenTelemetry SDK. /// - public static IOpenTelemetryBuilder AddElasticOpenTelemetry(this IServiceCollection serviceCollection, ElasticOpenTelemetryOptions options) + public static IOpenTelemetryBuilder AddElasticOpenTelemetry(this IServiceCollection services, ElasticOpenTelemetryBuilderOptions options) { - var descriptor = serviceCollection.SingleOrDefault(s => s.ServiceType == typeof(ElasticOpenTelemetryBuilder)); + var descriptor = services.SingleOrDefault(s => s.ServiceType == typeof(ElasticOpenTelemetryBuilder)); if (descriptor?.ImplementationInstance is ElasticOpenTelemetryBuilder builder) { @@ -44,6 +64,7 @@ public static IOpenTelemetryBuilder AddElasticOpenTelemetry(this IServiceCollect return builder; } + options = options.Services is null ? options with { Services = services } : options; return new ElasticOpenTelemetryBuilder(options); } } diff --git a/src/Elastic.OpenTelemetry/Diagnostics/LoggerMessages.cs b/src/Elastic.OpenTelemetry/Diagnostics/LoggerMessages.cs index 5f8c1da..b42818c 100644 --- a/src/Elastic.OpenTelemetry/Diagnostics/LoggerMessages.cs +++ b/src/Elastic.OpenTelemetry/Diagnostics/LoggerMessages.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information using System.Diagnostics; +using Elastic.OpenTelemetry.Configuration; using Elastic.OpenTelemetry.Diagnostics.Logging; using Microsoft.Extensions.Logging; @@ -65,8 +66,8 @@ public static void LogAgentPreamble(this ILogger logger) string[] environmentVariables = [ - EnvironmentVariables.ElasticOtelLogDirectoryEnvironmentVariable, - EnvironmentVariables.ElasticOtelLogLevelEnvironmentVariable + EnvironmentVariables.ElasticOtelFileLogDirectoryEnvironmentVariable, + EnvironmentVariables.ElasticOtelFileLogLevelEnvironmentVariable ]; foreach (var variable in environmentVariables) diff --git a/src/Elastic.OpenTelemetry/Diagnostics/Logging/AgentLoggingHelpers.cs b/src/Elastic.OpenTelemetry/Diagnostics/Logging/AgentLoggingHelpers.cs index 2864c2a..376e5c3 100644 --- a/src/Elastic.OpenTelemetry/Diagnostics/Logging/AgentLoggingHelpers.cs +++ b/src/Elastic.OpenTelemetry/Diagnostics/Logging/AgentLoggingHelpers.cs @@ -3,43 +3,26 @@ // See the LICENSE file in the project root for more information using System.Diagnostics; +using Elastic.OpenTelemetry.Configuration; using Microsoft.Extensions.Logging; namespace Elastic.OpenTelemetry.Diagnostics.Logging; internal static class AgentLoggingHelpers { - public static LogLevel GetElasticOtelLogLevel() - { - var logLevel = LogLevel.Information; - - var logLevelEnvironmentVariable = Environment.GetEnvironmentVariable(EnvironmentVariables.ElasticOtelLogLevelEnvironmentVariable); - - if (!string.IsNullOrEmpty(logLevelEnvironmentVariable)) - { - if (logLevelEnvironmentVariable.Equals(LogLevelHelpers.Trace, StringComparison.OrdinalIgnoreCase)) - logLevel = LogLevel.Trace; - - else if (logLevelEnvironmentVariable.Equals(LogLevelHelpers.Debug, StringComparison.OrdinalIgnoreCase)) - logLevel = LogLevel.Debug; + public static LogLevel DefaultLogLevel => LogLevel.Information; - else if (logLevelEnvironmentVariable.Equals(LogLevelHelpers.Info, StringComparison.OrdinalIgnoreCase)) - logLevel = LogLevel.Information; - - else if (logLevelEnvironmentVariable.Equals(LogLevelHelpers.Information, StringComparison.OrdinalIgnoreCase)) - logLevel = LogLevel.Information; - - else if (logLevelEnvironmentVariable.Equals(LogLevelHelpers.Warning, StringComparison.OrdinalIgnoreCase)) - logLevel = LogLevel.Warning; - - else if (logLevelEnvironmentVariable.Equals(LogLevelHelpers.Error, StringComparison.OrdinalIgnoreCase)) - logLevel = LogLevel.Error; + public static LogLevel GetElasticOtelLogLevelFromEnvironmentVariables() + { + var defaultLogLevel = DefaultLogLevel; - else if (logLevelEnvironmentVariable.Equals(LogLevelHelpers.Critical, StringComparison.OrdinalIgnoreCase)) - logLevel = LogLevel.Critical; - } + var logLevelEnvironmentVariable = Environment.GetEnvironmentVariable(EnvironmentVariables.ElasticOtelFileLogLevelEnvironmentVariable); - return logLevel; + if (string.IsNullOrEmpty(logLevelEnvironmentVariable)) + return defaultLogLevel; + + var parsedLogLevel = LogLevelHelpers.ToLogLevel(logLevelEnvironmentVariable); + return parsedLogLevel != LogLevel.None ? parsedLogLevel : defaultLogLevel; } public static void WriteLogLine(this ILogger logger, Activity? activity, int managedThreadId, DateTime dateTime, LogLevel logLevel, string logLine, string? spanId) diff --git a/src/Elastic.OpenTelemetry/Diagnostics/Logging/CompositeLogger.cs b/src/Elastic.OpenTelemetry/Diagnostics/Logging/CompositeLogger.cs index 3d4af29..ce57f86 100644 --- a/src/Elastic.OpenTelemetry/Diagnostics/Logging/CompositeLogger.cs +++ b/src/Elastic.OpenTelemetry/Diagnostics/Logging/CompositeLogger.cs @@ -2,6 +2,7 @@ // Elasticsearch B.V licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information +using Elastic.OpenTelemetry.Configuration; using Microsoft.Extensions.Logging; namespace Elastic.OpenTelemetry.Diagnostics.Logging; @@ -13,11 +14,11 @@ namespace Elastic.OpenTelemetry.Diagnostics.Logging; /// /// If disposed, triggers disposal of the . /// -internal sealed class CompositeLogger(ILogger? additionalLogger) : IDisposable, IAsyncDisposable, ILogger +internal sealed class CompositeLogger(ElasticOpenTelemetryBuilderOptions options) : IDisposable, IAsyncDisposable, ILogger { - public FileLogger FileLogger { get; } = new(); + public FileLogger FileLogger { get; } = new(options.DistroOptions); - private ILogger? _additionalLogger = additionalLogger; + private ILogger? _additionalLogger = options.Logger; private bool _isDisposed; public void Dispose() diff --git a/src/Elastic.OpenTelemetry/Diagnostics/Logging/FileLogger.cs b/src/Elastic.OpenTelemetry/Diagnostics/Logging/FileLogger.cs index 0630fee..2472d70 100644 --- a/src/Elastic.OpenTelemetry/Diagnostics/Logging/FileLogger.cs +++ b/src/Elastic.OpenTelemetry/Diagnostics/Logging/FileLogger.cs @@ -5,6 +5,7 @@ using System.Diagnostics; using System.Text; using System.Threading.Channels; +using Elastic.OpenTelemetry.Configuration; using Microsoft.Extensions.Logging; namespace Elastic.OpenTelemetry.Diagnostics.Logging; @@ -22,21 +23,33 @@ internal sealed class FileLogger : IDisposable, IAsyncDisposable, ILogger FullMode = BoundedChannelFullMode.Wait }); - private static readonly LogLevel ConfiguredLogLevel = AgentLoggingHelpers.GetElasticOtelLogLevel(); + private readonly LogLevel _configuredLogLevel; public bool FileLoggingEnabled { get; } private readonly LoggerExternalScopeProvider _scopeProvider; - public FileLogger() + public FileLogger(ElasticOpenTelemetryOptions options) { _scopeProvider = new LoggerExternalScopeProvider(); - var configuredPath = Environment.GetEnvironmentVariable(EnvironmentVariables.ElasticOtelLogDirectoryEnvironmentVariable); + var configuredPath = options.FileLogDirectory ?? Environment.GetEnvironmentVariable(EnvironmentVariables.ElasticOtelFileLogDirectoryEnvironmentVariable); if (string.IsNullOrEmpty(configuredPath)) return; + if (!string.IsNullOrEmpty(options.FileLogLevel)) + { + var logLevel = LogLevelHelpers.ToLogLevel(options.FileLogLevel); + + if (logLevel != LogLevel.None) + _configuredLogLevel = logLevel; + } + else + { + _configuredLogLevel = AgentLoggingHelpers.GetElasticOtelLogLevelFromEnvironmentVariables(); + } + var process = Process.GetCurrentProcess(); // When ordered by filename, we get see logs from the same process grouped, then ordered by oldest to newest, then the PID for that instance @@ -80,7 +93,7 @@ public void Log(LogLevel logLevel, EventId eventId, TState state, Except } } - public bool IsEnabled(LogLevel logLevel) => FileLoggingEnabled && ConfiguredLogLevel <= logLevel; + public bool IsEnabled(LogLevel logLevel) => FileLoggingEnabled && _configuredLogLevel <= logLevel; public IDisposable BeginScope(TState state) where TState : notnull => _scopeProvider.Push(state); diff --git a/src/Elastic.OpenTelemetry/Diagnostics/Logging/LogFormatter.cs b/src/Elastic.OpenTelemetry/Diagnostics/Logging/LogFormatter.cs index 25a35da..caf6895 100644 --- a/src/Elastic.OpenTelemetry/Diagnostics/Logging/LogFormatter.cs +++ b/src/Elastic.OpenTelemetry/Diagnostics/Logging/LogFormatter.cs @@ -87,7 +87,7 @@ private static void WriteLogPrefix(int managedThreadId, DateTime dateTime, LogLe .Append(']'); var length = builder.Length; - var padding = 52 - length; + var padding = 55 - length; for (var i = 0; i < padding; i++) builder.Append(' '); diff --git a/src/Elastic.OpenTelemetry/Diagnostics/Logging/LogLevelHelpers.cs b/src/Elastic.OpenTelemetry/Diagnostics/Logging/LogLevelHelpers.cs index c009233..a7db33f 100644 --- a/src/Elastic.OpenTelemetry/Diagnostics/Logging/LogLevelHelpers.cs +++ b/src/Elastic.OpenTelemetry/Diagnostics/Logging/LogLevelHelpers.cs @@ -10,24 +10,39 @@ internal static class LogLevelHelpers { public const string Critical = "Critical"; public const string Error = "Error"; - public const string Warning = "Warn"; - public const string Info = "Info"; + public const string Warning = "Warning"; public const string Information = "Information"; - public const string Trace = "Trace"; public const string Debug = "Debug"; + public const string Trace = "Trace"; + public const string None = "None"; - public static LogLevel ToLogLevel(string logLevelString) => - logLevelString switch - { - Critical => LogLevel.Critical, - Error => LogLevel.Error, - Warning => LogLevel.Warning, - Info => LogLevel.Information, - Information => LogLevel.Information, - Debug => LogLevel.Debug, - Trace => LogLevel.Trace, - _ => LogLevel.None, - }; + public static LogLevel ToLogLevel(string logLevelString) + { + var logLevel = LogLevel.None; + + if (logLevelString.Equals(Trace, StringComparison.OrdinalIgnoreCase)) + logLevel = LogLevel.Trace; + + else if (logLevelString.Equals(Debug, StringComparison.OrdinalIgnoreCase)) + logLevel = LogLevel.Debug; + + else if (logLevelString.Equals(Information, StringComparison.OrdinalIgnoreCase)) + logLevel = LogLevel.Information; + + else if (logLevelString.Equals(Information, StringComparison.OrdinalIgnoreCase)) + logLevel = LogLevel.Information; + + else if (logLevelString.Equals(Warning, StringComparison.OrdinalIgnoreCase)) + logLevel = LogLevel.Warning; + + else if (logLevelString.Equals(Error, StringComparison.OrdinalIgnoreCase)) + logLevel = LogLevel.Error; + + else if (logLevelString.Equals(Critical, StringComparison.OrdinalIgnoreCase)) + logLevel = LogLevel.Critical; + + return logLevel; + } public static string AsString(this LogLevel logLevel) => logLevel switch @@ -35,10 +50,10 @@ public static string AsString(this LogLevel logLevel) => LogLevel.Critical => Critical, LogLevel.Error => Error, LogLevel.Warning => Warning, - LogLevel.Information => Info, + LogLevel.Information => Information, LogLevel.Debug => Debug, LogLevel.Trace => Trace, - LogLevel.None => string.Empty, + LogLevel.None => None, _ => string.Empty }; } diff --git a/src/Elastic.OpenTelemetry/Diagnostics/LoggingEventListener.cs b/src/Elastic.OpenTelemetry/Diagnostics/LoggingEventListener.cs index 835323b..20a85cd 100644 --- a/src/Elastic.OpenTelemetry/Diagnostics/LoggingEventListener.cs +++ b/src/Elastic.OpenTelemetry/Diagnostics/LoggingEventListener.cs @@ -34,7 +34,7 @@ public LoggingEventListener(ILogger logger) { _logger = logger; - var eventLevel = AgentLoggingHelpers.GetElasticOtelLogLevel(); + var eventLevel = AgentLoggingHelpers.GetElasticOtelLogLevelFromEnvironmentVariables(); _eventLevel = eventLevel switch { diff --git a/src/Elastic.OpenTelemetry/ElasticOpenTelemetryBuilder.cs b/src/Elastic.OpenTelemetry/ElasticOpenTelemetryBuilder.cs index 151ee8f..33b2ecf 100644 --- a/src/Elastic.OpenTelemetry/ElasticOpenTelemetryBuilder.cs +++ b/src/Elastic.OpenTelemetry/ElasticOpenTelemetryBuilder.cs @@ -3,14 +3,14 @@ // See the LICENSE file in the project root for more information using System.Diagnostics; -using System.Linq.Expressions; -using System.Reflection; -using System.Runtime.InteropServices; +using Elastic.OpenTelemetry.Configuration; using Elastic.OpenTelemetry.Diagnostics; using Elastic.OpenTelemetry.Diagnostics.Logging; using Elastic.OpenTelemetry.Extensions; using Elastic.OpenTelemetry.Hosting; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using OpenTelemetry; using OpenTelemetry.Logs; @@ -52,58 +52,74 @@ public class ElasticOpenTelemetryBuilder : IOpenTelemetryBuilder /// Creates an instance of the configured with default options. /// public ElasticOpenTelemetryBuilder() - : this(new ElasticOpenTelemetryOptions()) + : this(new ElasticOpenTelemetryBuilderOptions()) { } /// /// Creates an instance of the configured with the provided - /// . + /// . /// - public ElasticOpenTelemetryBuilder(ElasticOpenTelemetryOptions options) + public ElasticOpenTelemetryBuilder(ElasticOpenTelemetryBuilderOptions options) { - Logger = new CompositeLogger(options.Logger); + Logger = new CompositeLogger(options); // Enables logging of OpenTelemetry-SDK event source events EventListener = new LoggingEventListener(Logger); Logger.LogAgentPreamble(); Logger.LogElasticOpenTelemetryBuilderInitialized(Environment.NewLine, new StackTrace(true)); + options.DistroOptions.LogConfigSources(Logger); + Services = options.Services ?? new ServiceCollection(); - if (options.Services != null) - Services.AddHostedService(); + if (options.Services is not null && !options.Services.Any(d => d.ImplementationType == typeof(ElasticOpenTelemetryService))) + Services.Insert(0, ServiceDescriptor.Singleton()); - Services.AddSingleton(this); + Services.TryAddSingleton(this); + // Directly invoke the SDK extension method to ensure SDK components are registered. var openTelemetry = Microsoft.Extensions.DependencyInjection.OpenTelemetryServicesExtensions.AddOpenTelemetry(Services); + // We always add this so we can identify a distro is being used, even if all Elastic defaults are disabled. + openTelemetry.ConfigureResource(r => r.AddDistroAttributes()); + + if (options.DistroOptions.EnabledDefaults.Equals(ElasticOpenTelemetryOptions.EnabledElasticDefaults.None)) + return; + //https://github.com/open-telemetry/opentelemetry-dotnet/pull/5400 - if (!options.SkipOtlpExporter) + if (!options.DistroOptions.SkipOtlpExporter) openTelemetry.UseOtlpExporter(); - //TODO Move to WithLogging once it gets stable - Services.Configure(logging => + if (options.DistroOptions.EnabledDefaults.HasFlag(ElasticOpenTelemetryOptions.EnabledElasticDefaults.Logging)) { - logging.IncludeFormattedMessage = true; - logging.IncludeScopes = true; - //TODO add processor that adds service.id - }); - - openTelemetry - .ConfigureResource(r => r.AddDistroAttributes()) - .WithTracing(tracing => + //TODO Move to WithLogging once it gets stable + Services.Configure(logging => { - tracing - .AddHttpClientInstrumentation() - .AddGrpcClientInstrumentation() - .AddEntityFrameworkCoreInstrumentation(); // TODO - Should we add this by default? + logging.IncludeFormattedMessage = true; + logging.IncludeScopes = true; + //TODO add processor that adds service.id + }); + } - tracing.AddElasticProcessors(Logger); + if (options.DistroOptions.EnabledDefaults.HasFlag(ElasticOpenTelemetryOptions.EnabledElasticDefaults.Tracing)) + { + openTelemetry.WithTracing(tracing => + { + tracing + .AddHttpClientInstrumentation() + .AddGrpcClientInstrumentation() + .AddEntityFrameworkCoreInstrumentation(); + + tracing.AddElasticProcessors(Logger); - Logger.LogConfiguredTracerProvider(); - }) - .WithMetrics(metrics => + Logger.LogConfiguredTracerProvider(); + }); + } + + if (options.DistroOptions.EnabledDefaults.HasFlag(ElasticOpenTelemetryOptions.EnabledElasticDefaults.Metrics)) + { + openTelemetry.WithMetrics(metrics => { metrics .AddProcessInstrumentation() @@ -112,6 +128,7 @@ public ElasticOpenTelemetryBuilder(ElasticOpenTelemetryOptions options) Logger.LogConfiguredMeterProvider(); }); + } } } diff --git a/src/Elastic.OpenTelemetry/EnvironmentVariables.cs b/src/Elastic.OpenTelemetry/EnvironmentVariables.cs deleted file mode 100644 index fa37ebc..0000000 --- a/src/Elastic.OpenTelemetry/EnvironmentVariables.cs +++ /dev/null @@ -1,11 +0,0 @@ -// Licensed to Elasticsearch B.V under one or more agreements. -// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. -// See the LICENSE file in the project root for more information - -namespace Elastic.OpenTelemetry; - -internal static class EnvironmentVariables -{ - public const string ElasticOtelLogDirectoryEnvironmentVariable = "ELASTIC_OTEL_LOG_DIRECTORY"; - public const string ElasticOtelLogLevelEnvironmentVariable = "ELASTIC_OTEL_LOG_LEVEL"; -} diff --git a/src/Elastic.OpenTelemetry/Extensions/OpenTelemetryBuilderExtensions.cs b/src/Elastic.OpenTelemetry/Extensions/OpenTelemetryBuilderExtensions.cs index 72ee6e8..b9ea121 100644 --- a/src/Elastic.OpenTelemetry/Extensions/OpenTelemetryBuilderExtensions.cs +++ b/src/Elastic.OpenTelemetry/Extensions/OpenTelemetryBuilderExtensions.cs @@ -33,7 +33,23 @@ public static IOpenTelemetryBuilder WithLogger(this IOpenTelemetryBuilder builde /// /// A new instance of which supports disposing of the /// OpenTelemetry providers to end signal collection. - public static IInstrumentationLifetime Build(this IOpenTelemetryBuilder builder, ILogger? logger = null, IServiceProvider? serviceProvider = null) + public static IInstrumentationLifetime Build(this IOpenTelemetryBuilder builder) + => builder.Build(null, null); + + /// + /// Triggers creation and registration of the OpenTelemetry components required to begin observing the application. + /// + /// A new instance of which supports disposing of the + /// OpenTelemetry providers to end signal collection. + public static IInstrumentationLifetime Build(this IOpenTelemetryBuilder builder, ILogger logger) + => builder.Build(logger, null); + + /// + /// Triggers creation and registration of the OpenTelemetry components required to begin observing the application. + /// + /// A new instance of which supports disposing of the + /// OpenTelemetry providers to end signal collection. + internal static IInstrumentationLifetime Build(this IOpenTelemetryBuilder builder, ILogger? logger = null, IServiceProvider? serviceProvider = null) { // this happens if someone calls Build() while using IServiceCollection and AddOpenTelemetry() and NOT Add*Elastic*OpenTelemetry() // we treat this a NOOP diff --git a/src/Elastic.OpenTelemetry/InstrumentationLifetime.cs b/src/Elastic.OpenTelemetry/InstrumentationLifetime.cs index 4377738..4e99580 100644 --- a/src/Elastic.OpenTelemetry/InstrumentationLifetime.cs +++ b/src/Elastic.OpenTelemetry/InstrumentationLifetime.cs @@ -12,22 +12,22 @@ namespace Elastic.OpenTelemetry; internal class InstrumentationLifetime( CompositeLogger logger, LoggingEventListener loggingEventListener, - TracerProvider tracerProvider, - MeterProvider meterProvider + TracerProvider? tracerProvider, + MeterProvider? meterProvider ) : IInstrumentationLifetime { public void Dispose() { - tracerProvider.Dispose(); - meterProvider.Dispose(); + tracerProvider?.Dispose(); + meterProvider?.Dispose(); loggingEventListener.Dispose(); logger.Dispose(); } public async ValueTask DisposeAsync() { - tracerProvider.Dispose(); - meterProvider.Dispose(); + tracerProvider?.Dispose(); + meterProvider?.Dispose(); await loggingEventListener.DisposeAsync().ConfigureAwait(false); await logger.DisposeAsync().ConfigureAwait(false); } diff --git a/tests/Elastic.OpenTelemetry.Tests/Configuration/ElasticOpenTelemetryOptionsTests.cs b/tests/Elastic.OpenTelemetry.Tests/Configuration/ElasticOpenTelemetryOptionsTests.cs new file mode 100644 index 0000000..92f88a3 --- /dev/null +++ b/tests/Elastic.OpenTelemetry.Tests/Configuration/ElasticOpenTelemetryOptionsTests.cs @@ -0,0 +1,359 @@ +// Licensed to Elasticsearch B.V under one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using System.Text; +using Elastic.OpenTelemetry.Configuration; +using Elastic.OpenTelemetry.Extensions; +using Microsoft.Extensions.Configuration; +using OpenTelemetry; +using Xunit.Abstractions; + +using static Elastic.OpenTelemetry.Configuration.ElasticOpenTelemetryOptions; + +namespace Elastic.OpenTelemetry.Tests.Configuration; + +public sealed class ElasticOpenTelemetryOptionsTests(ITestOutputHelper output) : IDisposable +{ + private readonly ITestOutputHelper _output = output; + private readonly string? _originalFileLogDirectoryEnvVar = Environment.GetEnvironmentVariable(EnvironmentVariables.ElasticOtelFileLogDirectoryEnvironmentVariable); + private readonly string? _originalFileLogLevelEnvVar = Environment.GetEnvironmentVariable(EnvironmentVariables.ElasticOtelFileLogLevelEnvironmentVariable); + private readonly string? _originalEnableElasticDefaultsEnvVar = Environment.GetEnvironmentVariable(EnvironmentVariables.ElasticOtelEnableElasticDefaults); + private readonly string? _originalSkipOtlpExporterEnvVar = Environment.GetEnvironmentVariable(EnvironmentVariables.ElasticOtelSkipOtlpExporter); + + [Fact] + public void EnabledElasticDefaults_NoneIncludesExpectedValues() + { + var sut = EnabledElasticDefaults.None; + + sut.HasFlag(EnabledElasticDefaults.Tracing).Should().BeFalse(); + sut.HasFlag(EnabledElasticDefaults.Logging).Should().BeFalse(); + sut.HasFlag(EnabledElasticDefaults.Metrics).Should().BeFalse(); + } + + [Fact] + public void DefaultCtor_SetsExpectedDefaults_WhenNoEnvironmentVariablesAreConfigured() + { + Environment.SetEnvironmentVariable(EnvironmentVariables.ElasticOtelFileLogDirectoryEnvironmentVariable, null); + Environment.SetEnvironmentVariable(EnvironmentVariables.ElasticOtelFileLogLevelEnvironmentVariable, null); + Environment.SetEnvironmentVariable(EnvironmentVariables.ElasticOtelEnableElasticDefaults, null); + Environment.SetEnvironmentVariable(EnvironmentVariables.ElasticOtelSkipOtlpExporter, null); + + var sut = new ElasticOpenTelemetryOptions(); + + sut.FileLogDirectory.Should().Be(string.Empty); + sut.FileLogLevel.Should().Be(string.Empty); + sut.EnableElasticDefaults.Should().Be(string.Empty); + sut.EnabledDefaults.Should().HaveFlag(EnabledElasticDefaults.Tracing); + sut.EnabledDefaults.Should().HaveFlag(EnabledElasticDefaults.Metrics); + sut.EnabledDefaults.Should().HaveFlag(EnabledElasticDefaults.Logging); + sut.SkipOtlpExporter.Should().Be(false); + + var logger = new TestLogger(_output); + + sut.LogConfigSources(logger); + + logger.Messages.Count.Should().Be(4); + foreach (var message in logger.Messages) + { + message.Should().EndWith("from [Default]"); + } + + ResetEnvironmentVariables(); + } + + [Fact] + public void DefaultCtor_LoadsConfigurationFromEnvironmentVariables() + { + const string fileLogDirectory = "C:\\Temp"; + const string fileLogLevel = "Critical"; + const string enabledElasticDefaults = "None"; + + Environment.SetEnvironmentVariable(EnvironmentVariables.ElasticOtelFileLogDirectoryEnvironmentVariable, fileLogDirectory); + Environment.SetEnvironmentVariable(EnvironmentVariables.ElasticOtelFileLogLevelEnvironmentVariable, fileLogLevel); + Environment.SetEnvironmentVariable(EnvironmentVariables.ElasticOtelEnableElasticDefaults, enabledElasticDefaults); + Environment.SetEnvironmentVariable(EnvironmentVariables.ElasticOtelSkipOtlpExporter, "true"); + + var sut = new ElasticOpenTelemetryOptions(); + + sut.FileLogDirectory.Should().Be(fileLogDirectory); + sut.FileLogLevel.Should().Be(fileLogLevel); + sut.EnableElasticDefaults.Should().Be(enabledElasticDefaults); + sut.EnabledDefaults.Should().Be(EnabledElasticDefaults.None); + sut.SkipOtlpExporter.Should().Be(true); + + var logger = new TestLogger(_output); + + sut.LogConfigSources(logger); + + logger.Messages.Count.Should().Be(4); + foreach (var message in logger.Messages) + { + message.Should().EndWith("from [Environment]"); + } + + ResetEnvironmentVariables(); + } + + [Fact] + public void ConfigurationCtor_LoadsConfigurationFromIConfiguration() + { + const string fileLogLevel = "Critical"; + const string enabledElasticDefaults = "None"; + + // Remove all env vars + Environment.SetEnvironmentVariable(EnvironmentVariables.ElasticOtelFileLogDirectoryEnvironmentVariable, null); + Environment.SetEnvironmentVariable(EnvironmentVariables.ElasticOtelFileLogLevelEnvironmentVariable, null); + Environment.SetEnvironmentVariable(EnvironmentVariables.ElasticOtelEnableElasticDefaults, null); + Environment.SetEnvironmentVariable(EnvironmentVariables.ElasticOtelSkipOtlpExporter, null); + + var json = $$""" + { + "Elastic": { + "OpenTelemetry": { + "FileLogDirectory": "C:\\Temp", + "FileLogLevel": "{{fileLogLevel}}", + "EnabledElasticDefaults": "{{enabledElasticDefaults}}", + "SkipOtlpExporter": true + } + } + } + """; + + var config = new ConfigurationBuilder() + .AddJsonStream(new MemoryStream(Encoding.UTF8.GetBytes(json))) + .Build(); + + var sut = new ElasticOpenTelemetryOptions(config); + + sut.FileLogDirectory.Should().Be(@"C:\Temp"); + sut.FileLogLevel.Should().Be(fileLogLevel); + sut.EnableElasticDefaults.Should().Be(enabledElasticDefaults); + sut.EnabledDefaults.Should().Be(EnabledElasticDefaults.None); + sut.SkipOtlpExporter.Should().Be(true); + + var logger = new TestLogger(_output); + + sut.LogConfigSources(logger); + + logger.Messages.Count.Should().Be(4); + foreach (var message in logger.Messages) + { + message.Should().EndWith("from [IConfiguration]"); + } + + ResetEnvironmentVariables(); + } + + [Fact] + public void EnvironmentVariables_TakePrecedenceOver_ConfigValues() + { + const string fileLogDirectory = "C:\\Temp"; + const string fileLogLevel = "Critical"; + const string enabledElasticDefaults = "None"; + + Environment.SetEnvironmentVariable(EnvironmentVariables.ElasticOtelFileLogDirectoryEnvironmentVariable, fileLogDirectory); + Environment.SetEnvironmentVariable(EnvironmentVariables.ElasticOtelFileLogLevelEnvironmentVariable, fileLogLevel); + Environment.SetEnvironmentVariable(EnvironmentVariables.ElasticOtelEnableElasticDefaults, enabledElasticDefaults); + Environment.SetEnvironmentVariable(EnvironmentVariables.ElasticOtelSkipOtlpExporter, "true"); + + var json = $$""" + { + "Elastic": { + "OpenTelemetry": { + "FileLogDirectory": "C:\\Json", + "FileLogLevel": "Trace", + "EnabledElasticDefaults": "All", + "SkipOtlpExporter": false + } + } + } + """; + + var config = new ConfigurationBuilder() + .AddJsonStream(new MemoryStream(Encoding.UTF8.GetBytes(json))) + .Build(); + + var sut = new ElasticOpenTelemetryOptions(config); + + sut.FileLogDirectory.Should().Be(fileLogDirectory); + sut.FileLogLevel.Should().Be(fileLogLevel); + sut.EnableElasticDefaults.Should().Be(enabledElasticDefaults); + sut.EnabledDefaults.Should().Be(EnabledElasticDefaults.None); + sut.SkipOtlpExporter.Should().Be(true); + + ResetEnvironmentVariables(); + } + + [Fact] + public void InitializedProperties_TakePrecedenceOver_EnvironmentValues() + { + const string fileLogDirectory = "C:\\Property"; + const string fileLogLevel = "Critical"; + const string enabledElasticDefaults = "None"; + + Environment.SetEnvironmentVariable(EnvironmentVariables.ElasticOtelFileLogDirectoryEnvironmentVariable, "C:\\Temp"); + Environment.SetEnvironmentVariable(EnvironmentVariables.ElasticOtelFileLogLevelEnvironmentVariable, "Information"); + Environment.SetEnvironmentVariable(EnvironmentVariables.ElasticOtelEnableElasticDefaults, "All"); + Environment.SetEnvironmentVariable(EnvironmentVariables.ElasticOtelSkipOtlpExporter, "true"); + + var sut = new ElasticOpenTelemetryOptions + { + FileLogDirectory = fileLogDirectory, + FileLogLevel = fileLogLevel, + SkipOtlpExporter = false, + EnableElasticDefaults = enabledElasticDefaults + }; + + sut.FileLogDirectory.Should().Be(fileLogDirectory); + sut.FileLogLevel.Should().Be(fileLogLevel); + sut.EnableElasticDefaults.Should().Be(enabledElasticDefaults); + sut.EnabledDefaults.Should().Be(EnabledElasticDefaults.None); + sut.SkipOtlpExporter.Should().Be(false); + + var logger = new TestLogger(_output); + + sut.LogConfigSources(logger); + + logger.Messages.Count.Should().Be(4); + foreach (var message in logger.Messages) + { + message.Should().EndWith("from [Property]"); + } + + ResetEnvironmentVariables(); + } + + [Theory] + [ClassData(typeof(DefaultsData))] + internal void ElasticDefaults_ConvertsAsExpected(string optionValue, Action asserts) + { + var sut = new ElasticOpenTelemetryOptions + { + EnableElasticDefaults = optionValue + }; + + asserts(sut.EnabledDefaults); + } + + internal class DefaultsData : TheoryData> + { + public DefaultsData() + { + Add("All", a => + { + a.HasFlag(EnabledElasticDefaults.Tracing).Should().BeTrue(); + a.HasFlag(EnabledElasticDefaults.Metrics).Should().BeTrue(); + a.HasFlag(EnabledElasticDefaults.Logging).Should().BeTrue(); + a.Equals(EnabledElasticDefaults.None).Should().BeFalse(); + }); + + Add("all", a => + { + a.HasFlag(EnabledElasticDefaults.Tracing).Should().BeTrue(); + a.HasFlag(EnabledElasticDefaults.Metrics).Should().BeTrue(); + a.HasFlag(EnabledElasticDefaults.Logging).Should().BeTrue(); + a.Equals(EnabledElasticDefaults.None).Should().BeFalse(); + }); + + Add("Tracing", a => + { + a.HasFlag(EnabledElasticDefaults.Tracing).Should().BeTrue(); + a.HasFlag(EnabledElasticDefaults.Metrics).Should().BeFalse(); + a.HasFlag(EnabledElasticDefaults.Logging).Should().BeFalse(); + a.Equals(EnabledElasticDefaults.None).Should().BeFalse(); + }); + + Add("Metrics", a => + { + a.HasFlag(EnabledElasticDefaults.Tracing).Should().BeFalse(); + a.HasFlag(EnabledElasticDefaults.Metrics).Should().BeTrue(); + a.HasFlag(EnabledElasticDefaults.Logging).Should().BeFalse(); + a.Equals(EnabledElasticDefaults.None).Should().BeFalse(); + }); + + Add("Logging", a => + { + a.HasFlag(EnabledElasticDefaults.Tracing).Should().BeFalse(); + a.HasFlag(EnabledElasticDefaults.Metrics).Should().BeFalse(); + a.HasFlag(EnabledElasticDefaults.Logging).Should().BeTrue(); + a.Equals(EnabledElasticDefaults.None).Should().BeFalse(); + }); + + Add("Tracing,Logging", a => + { + a.HasFlag(EnabledElasticDefaults.Tracing).Should().BeTrue(); + a.HasFlag(EnabledElasticDefaults.Metrics).Should().BeFalse(); + a.HasFlag(EnabledElasticDefaults.Logging).Should().BeTrue(); + a.Equals(EnabledElasticDefaults.None).Should().BeFalse(); + }); + + Add("tracing,logging,metrics", a => + { + a.HasFlag(EnabledElasticDefaults.Tracing).Should().BeTrue(); + a.HasFlag(EnabledElasticDefaults.Metrics).Should().BeTrue(); + a.HasFlag(EnabledElasticDefaults.Logging).Should().BeTrue(); + a.Equals(EnabledElasticDefaults.None).Should().BeFalse(); + }); + + Add("None", a => + { + a.HasFlag(EnabledElasticDefaults.Tracing).Should().BeFalse(); + a.HasFlag(EnabledElasticDefaults.Metrics).Should().BeFalse(); + a.HasFlag(EnabledElasticDefaults.Logging).Should().BeFalse(); + a.Equals(EnabledElasticDefaults.None).Should().BeTrue(); + }); + } + }; + + [Fact] + public void TransactionId_IsNotAdded_WhenElasticDefaultsDoesNotIncludeTracing() + { + var options = new ElasticOpenTelemetryBuilderOptions + { + Logger = new TestLogger(_output), + DistroOptions = new ElasticOpenTelemetryOptions() + { + SkipOtlpExporter = true, + EnableElasticDefaults = "None" + } + }; + + const string activitySourceName = nameof(TransactionId_IsNotAdded_WhenElasticDefaultsDoesNotIncludeTracing); + + var activitySource = new ActivitySource(activitySourceName, "1.0.0"); + + var exportedItems = new List(); + + using var session = new ElasticOpenTelemetryBuilder(options) + .WithTracing(tpb => + { + tpb + .ConfigureResource(rb => rb.AddService("Test", "1.0.0")) + .AddSource(activitySourceName) + .AddInMemoryExporter(exportedItems); + }) + .Build(); + + using (var activity = activitySource.StartActivity(ActivityKind.Internal)) + activity?.SetStatus(ActivityStatusCode.Ok); + + exportedItems.Should().ContainSingle(); + + var exportedActivity = exportedItems[0]; + + var transactionId = exportedActivity.GetTagItem(TransactionIdProcessor.TransactionIdTagName); + + transactionId.Should().BeNull(); + } + + private void ResetEnvironmentVariables() + { + Environment.SetEnvironmentVariable(EnvironmentVariables.ElasticOtelFileLogDirectoryEnvironmentVariable, _originalFileLogDirectoryEnvVar); + Environment.SetEnvironmentVariable(EnvironmentVariables.ElasticOtelFileLogLevelEnvironmentVariable, _originalFileLogLevelEnvVar); + Environment.SetEnvironmentVariable(EnvironmentVariables.ElasticOtelEnableElasticDefaults, _originalEnableElasticDefaultsEnvVar); + Environment.SetEnvironmentVariable(EnvironmentVariables.ElasticOtelSkipOtlpExporter, _originalSkipOtlpExporterEnvVar); + } + + public void Dispose() => ResetEnvironmentVariables(); +} diff --git a/tests/Elastic.OpenTelemetry.Tests/LoggingTests.cs b/tests/Elastic.OpenTelemetry.Tests/LoggingTests.cs index 7b35134..69f0743 100644 --- a/tests/Elastic.OpenTelemetry.Tests/LoggingTests.cs +++ b/tests/Elastic.OpenTelemetry.Tests/LoggingTests.cs @@ -2,6 +2,7 @@ // Elasticsearch B.V licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information +using Elastic.OpenTelemetry.Configuration; using Elastic.OpenTelemetry.Extensions; using OpenTelemetry; using Xunit.Abstractions; @@ -14,7 +15,7 @@ public class LoggingTests(ITestOutputHelper output) public async Task ObserveLogging() { var logger = new TestLogger(output); - var options = new ElasticOpenTelemetryOptions { Logger = logger, SkipOtlpExporter = true }; + var options = new ElasticOpenTelemetryBuilderOptions { Logger = logger, DistroOptions = new() { SkipOtlpExporter = true } }; const string activitySourceName = nameof(ObserveLogging); var activitySource = new ActivitySource(activitySourceName, "1.0.0"); @@ -34,13 +35,13 @@ public async Task ObserveLogging() //assert preamble information gets logged logger.Messages.Should().ContainMatch("*Elastic OpenTelemetry Distribution:*"); - var preambles = logger.Messages.Where(l => l.Contains("[Info] Elastic OpenTelemetry Distribution:")).ToList(); + var preambles = logger.Messages.Where(l => l.Contains("[Information] Elastic OpenTelemetry Distribution:")).ToList(); preambles.Should().NotBeNull().And.HaveCount(1); // assert instrumentation session logs initialized and stack trace gets dumped. logger.Messages.Should().ContainMatch("*ElasticOpenTelemetryBuilder initialized*"); // very lenient format check - logger.Messages.Should().ContainMatch("[*][*][*][Info]*"); + logger.Messages.Should().ContainMatch("[*][*][*][Information]*"); } } diff --git a/tests/Elastic.OpenTelemetry.Tests/ServiceCollectionTests.cs b/tests/Elastic.OpenTelemetry.Tests/ServiceCollectionTests.cs index d0064f2..8495c35 100644 --- a/tests/Elastic.OpenTelemetry.Tests/ServiceCollectionTests.cs +++ b/tests/Elastic.OpenTelemetry.Tests/ServiceCollectionTests.cs @@ -2,6 +2,7 @@ // Elasticsearch B.V licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information +using Elastic.OpenTelemetry.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using OpenTelemetry; @@ -14,7 +15,11 @@ public class ServiceCollectionTests(ITestOutputHelper output) [Fact] public async Task ServiceCollectionAddIsSafeToCallMultipleTimes() { - var options = new ElasticOpenTelemetryOptions { Logger = new TestLogger(output), SkipOtlpExporter = true }; + var options = new ElasticOpenTelemetryBuilderOptions + { + Logger = new TestLogger(output), + DistroOptions = new ElasticOpenTelemetryOptions() { SkipOtlpExporter = true } + }; const string activitySourceName = nameof(ServiceCollectionAddIsSafeToCallMultipleTimes); var activitySource = new ActivitySource(activitySourceName, "1.0.0"); @@ -24,7 +29,7 @@ public async Task ServiceCollectionAddIsSafeToCallMultipleTimes() var host = Host.CreateDefaultBuilder(); host.ConfigureServices(s => { - s.AddOpenTelemetry(options) + s.AddElasticOpenTelemetry(options) .WithTracing(tpb => tpb .ConfigureResource(rb => rb.AddService("Test", "1.0.0")) .AddSource(activitySourceName) diff --git a/tests/Elastic.OpenTelemetry.Tests/TestLogger.cs b/tests/Elastic.OpenTelemetry.Tests/TestLogger.cs index 0c475d5..5a89eca 100644 --- a/tests/Elastic.OpenTelemetry.Tests/TestLogger.cs +++ b/tests/Elastic.OpenTelemetry.Tests/TestLogger.cs @@ -10,7 +10,8 @@ namespace Elastic.OpenTelemetry.Tests; public class TestLogger(ITestOutputHelper testOutputHelper) : ILogger { - private readonly List _messages = new(); + private readonly List _messages = []; + public IReadOnlyCollection Messages => _messages.AsReadOnly(); public IDisposable BeginScope(TState state) where TState : notnull => NoopDisposable.Instance; diff --git a/tests/Elastic.OpenTelemetry.Tests/TransactionIdProcessorTests.cs b/tests/Elastic.OpenTelemetry.Tests/TransactionIdProcessorTests.cs index 57ab9bd..3bc050b 100644 --- a/tests/Elastic.OpenTelemetry.Tests/TransactionIdProcessorTests.cs +++ b/tests/Elastic.OpenTelemetry.Tests/TransactionIdProcessorTests.cs @@ -2,6 +2,7 @@ // Elasticsearch B.V licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information +using Elastic.OpenTelemetry.Configuration; using Elastic.OpenTelemetry.Extensions; using OpenTelemetry; using Xunit.Abstractions; @@ -13,7 +14,7 @@ public class TransactionIdProcessorTests(ITestOutputHelper output) [Fact] public void TransactionId_IsAddedToTags() { - var options = new ElasticOpenTelemetryOptions { Logger = new TestLogger(output), SkipOtlpExporter = true }; + var options = new ElasticOpenTelemetryBuilderOptions { Logger = new TestLogger(output), DistroOptions = new ElasticOpenTelemetryOptions() { SkipOtlpExporter = true } }; const string activitySourceName = nameof(TransactionId_IsAddedToTags); var activitySource = new ActivitySource(activitySourceName, "1.0.0"); @@ -33,7 +34,7 @@ public void TransactionId_IsAddedToTags() using (var activity = activitySource.StartActivity(ActivityKind.Internal)) activity?.SetStatus(ActivityStatusCode.Ok); - exportedItems.Should().HaveCount(1); + exportedItems.Should().ContainSingle(); var exportedActivity = exportedItems[0];