diff --git a/src/Elastic.OpenTelemetry/Configuration/ElasticOpenTelemetryOptions.cs b/src/Elastic.OpenTelemetry/Configuration/ElasticOpenTelemetryOptions.cs index 5f9a2ef..507b177 100644 --- a/src/Elastic.OpenTelemetry/Configuration/ElasticOpenTelemetryOptions.cs +++ b/src/Elastic.OpenTelemetry/Configuration/ElasticOpenTelemetryOptions.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.Diagnostics.Logging; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; @@ -40,6 +41,8 @@ public class ElasticOpenTelemetryOptions private string? _enabledElasticDefaults; private ConfigSource _enabledElasticDefaultsSource = ConfigSource.Default; + private string? _loggingSectionLogLevel; + /// /// Creates a new instance of with properties /// bound from environment variables. @@ -62,14 +65,36 @@ public ElasticOpenTelemetryOptions() /// 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); + if (configuration is not null) + { + 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); + + BindFromLoggingSection(configuration); + } + + void BindFromLoggingSection(IConfiguration configuration) + { + // This will be used as a fallback if a more specific configuration is not provided. + // We also store the logging level to use it within the logging event listener to determine the most verbose level to subscribe to. + _loggingSectionLogLevel = configuration.GetValue($"Logging:LogLevel:{CompositeLogger.LogCategory}"); + + // Fall back to the default logging level if the specific category is not configured. + if (string.IsNullOrEmpty(_loggingSectionLogLevel)) + _loggingSectionLogLevel = configuration.GetValue("Logging:LogLevel:Default"); + + if (!string.IsNullOrEmpty(_loggingSectionLogLevel) && _fileLogLevel is null) + { + _fileLogLevel = _loggingSectionLogLevel; + _fileLogLevelSource = ConfigSource.IConfiguration; + } + } } /// @@ -151,6 +176,8 @@ public string EnableElasticDefaults } } + internal string? LoggingSectionLogLevel => _loggingSectionLogLevel; + internal EnabledElasticDefaults EnabledDefaults => _elasticDefaults ?? GetEnabledElasticDefaults(); private static (bool, string) StringParser(string? s) => !string.IsNullOrEmpty(s) ? (true, s) : (false, string.Empty); @@ -173,7 +200,7 @@ private static void SetFromConfiguration(IConfiguration configuration, string { if (field is null) { - var logFileDirectory = configuration?.GetValue($"{ConfigurationSection}:{key}"); + var logFileDirectory = configuration.GetValue($"{ConfigurationSection}:{key}"); var (success, value) = parser(logFileDirectory); diff --git a/src/Elastic.OpenTelemetry/Diagnostics/Logging/CompositeLogger.cs b/src/Elastic.OpenTelemetry/Diagnostics/Logging/CompositeLogger.cs index ce57f86..83ec309 100644 --- a/src/Elastic.OpenTelemetry/Diagnostics/Logging/CompositeLogger.cs +++ b/src/Elastic.OpenTelemetry/Diagnostics/Logging/CompositeLogger.cs @@ -16,6 +16,8 @@ namespace Elastic.OpenTelemetry.Diagnostics.Logging; /// internal sealed class CompositeLogger(ElasticOpenTelemetryBuilderOptions options) : IDisposable, IAsyncDisposable, ILogger { + public const string LogCategory = "Elastic.OpenTelemetry"; + public FileLogger FileLogger { get; } = new(options.DistroOptions); private ILogger? _additionalLogger = options.Logger; diff --git a/src/Elastic.OpenTelemetry/Diagnostics/LoggingEventListener.cs b/src/Elastic.OpenTelemetry/Diagnostics/LoggingEventListener.cs index 20a85cd..6dff1b8 100644 --- a/src/Elastic.OpenTelemetry/Diagnostics/LoggingEventListener.cs +++ b/src/Elastic.OpenTelemetry/Diagnostics/LoggingEventListener.cs @@ -7,6 +7,7 @@ using System.Text.RegularExpressions; using Elastic.OpenTelemetry.Diagnostics.Logging; using Microsoft.Extensions.Logging; +using Elastic.OpenTelemetry.Configuration; namespace Elastic.OpenTelemetry.Diagnostics; @@ -26,15 +27,25 @@ class LoggingEventListener : EventListener, IAsyncDisposable [GeneratedRegex(TraceParentRegularExpressionString)] private static partial Regex TraceParentRegex(); #else - private static readonly Regex _traceParentRegex = new Regex(TraceParentRegularExpressionString); + private static readonly Regex _traceParentRegex = new(TraceParentRegularExpressionString); private static Regex TraceParentRegex() => _traceParentRegex; #endif - public LoggingEventListener(ILogger logger) + public LoggingEventListener(ILogger logger, ElasticOpenTelemetryOptions options) { _logger = logger; - var eventLevel = AgentLoggingHelpers.GetElasticOtelLogLevelFromEnvironmentVariables(); + // When both a file log level and a logging section log level are provided, the more verbose of the two is used. + // This insures we subscribes to the lowest level of events needed. + // The specific loggers will then determine if they should log the event based on their own log level. + var eventLevel = LogLevelHelpers.ToLogLevel(options.FileLogLevel); + if (!string.IsNullOrEmpty(options.LoggingSectionLogLevel)) + { + var logLevel = LogLevelHelpers.ToLogLevel(options.LoggingSectionLogLevel); + + if (logLevel < eventLevel) + eventLevel = logLevel; + } _eventLevel = eventLevel switch { diff --git a/src/Elastic.OpenTelemetry/ElasticOpenTelemetryBuilder.cs b/src/Elastic.OpenTelemetry/ElasticOpenTelemetryBuilder.cs index 33b2ecf..3fd7ef6 100644 --- a/src/Elastic.OpenTelemetry/ElasticOpenTelemetryBuilder.cs +++ b/src/Elastic.OpenTelemetry/ElasticOpenTelemetryBuilder.cs @@ -64,7 +64,7 @@ public ElasticOpenTelemetryBuilder(ElasticOpenTelemetryBuilderOptions options) Logger = new CompositeLogger(options); // Enables logging of OpenTelemetry-SDK event source events - EventListener = new LoggingEventListener(Logger); + EventListener = new LoggingEventListener(Logger, options.DistroOptions); Logger.LogAgentPreamble(); Logger.LogElasticOpenTelemetryBuilderInitialized(Environment.NewLine, new StackTrace(true)); diff --git a/src/Elastic.OpenTelemetry/Hosting/ElasticOpenTelemetryService.cs b/src/Elastic.OpenTelemetry/Hosting/ElasticOpenTelemetryService.cs index c8c0c0c..203b1f4 100644 --- a/src/Elastic.OpenTelemetry/Hosting/ElasticOpenTelemetryService.cs +++ b/src/Elastic.OpenTelemetry/Hosting/ElasticOpenTelemetryService.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.Diagnostics.Logging; using Elastic.OpenTelemetry.Extensions; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; @@ -16,7 +17,7 @@ internal sealed class ElasticOpenTelemetryService(IServiceProvider serviceProvid public Task StartingAsync(CancellationToken cancellationToken) { var loggerFactory = serviceProvider.GetService(); - var logger = loggerFactory?.CreateLogger($"{nameof(Elastic)}.{nameof(OpenTelemetry)}"); + var logger = loggerFactory?.CreateLogger(CompositeLogger.LogCategory); _lifeTime = serviceProvider.GetRequiredService().Build(logger, serviceProvider); diff --git a/tests/Elastic.OpenTelemetry.Tests/Configuration/ElasticOpenTelemetryOptionsTests.cs b/tests/Elastic.OpenTelemetry.Tests/Configuration/ElasticOpenTelemetryOptionsTests.cs index e053460..a351fd6 100644 --- a/tests/Elastic.OpenTelemetry.Tests/Configuration/ElasticOpenTelemetryOptionsTests.cs +++ b/tests/Elastic.OpenTelemetry.Tests/Configuration/ElasticOpenTelemetryOptionsTests.cs @@ -98,6 +98,7 @@ public void DefaultCtor_LoadsConfigurationFromEnvironmentVariables() [Fact] public void ConfigurationCtor_LoadsConfigurationFromIConfiguration() { + const string loggingSectionLogLevel = "Warning"; const string fileLogLevel = "Critical"; const string enabledElasticDefaults = "None"; @@ -109,6 +110,12 @@ public void ConfigurationCtor_LoadsConfigurationFromIConfiguration() var json = $$""" { + "Logging": { + "LogLevel": { + "Default": "Information", + "Elastic.OpenTelemetry": "{{loggingSectionLogLevel}}" + } + }, "Elastic": { "OpenTelemetry": { "FileLogDirectory": "C:\\Temp", @@ -131,6 +138,118 @@ public void ConfigurationCtor_LoadsConfigurationFromIConfiguration() sut.EnableElasticDefaults.Should().Be(enabledElasticDefaults); sut.EnabledDefaults.Should().Be(EnabledElasticDefaults.None); sut.SkipOtlpExporter.Should().Be(true); + sut.LoggingSectionLogLevel.Should().Be(loggingSectionLogLevel); + + 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 ConfigurationCtor_LoadsConfigurationFromIConfiguration_AndFallsBackToLoggingSection_WhenAvailable() + { + const string loggingSectionLogLevel = "Warning"; + 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 = $$""" + { + "Logging": { + "LogLevel": { + "Default": "Information", + "Elastic.OpenTelemetry": "{{loggingSectionLogLevel}}" + } + }, + "Elastic": { + "OpenTelemetry": { + "FileLogDirectory": "C:\\Temp", + "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(loggingSectionLogLevel); + sut.EnableElasticDefaults.Should().Be(enabledElasticDefaults); + sut.EnabledDefaults.Should().Be(EnabledElasticDefaults.None); + sut.SkipOtlpExporter.Should().Be(true); + sut.LoggingSectionLogLevel.Should().Be(loggingSectionLogLevel); + + 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 ConfigurationCtor_LoadsConfigurationFromIConfiguration_AndFallsBackToLoggingSectionDefault_WhenAvailable() + { + const string loggingSectionDefaultLogLevel = "Information"; + 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 = $$""" + { + "Logging": { + "LogLevel": { + "Default": "{{loggingSectionDefaultLogLevel}}" + } + }, + "Elastic": { + "OpenTelemetry": { + "FileLogDirectory": "C:\\Temp", + "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(loggingSectionDefaultLogLevel); + sut.EnableElasticDefaults.Should().Be(enabledElasticDefaults); + sut.EnabledDefaults.Should().Be(EnabledElasticDefaults.None); + sut.SkipOtlpExporter.Should().Be(true); + sut.LoggingSectionLogLevel.Should().Be(loggingSectionDefaultLogLevel); var logger = new TestLogger(_output);