From 6de3d9437d556f2b0b247eb337721310c082c18f Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Tue, 4 Jun 2024 17:26:41 +0200 Subject: [PATCH] Update tests to ensure defaults match existing agent --- examples/Example.MinimalApi/appsettings.json | 4 +- .../ElasticOpenTelemetryOptions.cs | 150 ++++++++++---- .../Configuration/EnvironmentVariables.cs | 1 + .../Configuration/LogTargets.cs | 26 +++ .../Logging/AgentLoggingHelpers.cs | 16 -- .../Diagnostics/Logging/FileLogger.cs | 11 +- .../Diagnostics/Logging/LogLevelHelpers.cs | 43 ++-- .../Diagnostics/LoggingEventListener.cs | 4 +- .../ElasticOpenTelemetryOptionsTests.cs | 187 +++++++----------- .../GlobalLogConfigurationTests.cs | 147 ++++++++++++++ 10 files changed, 385 insertions(+), 204 deletions(-) create mode 100644 src/Elastic.OpenTelemetry/Configuration/LogTargets.cs create mode 100644 tests/Elastic.OpenTelemetry.Tests/Configuration/GlobalLogConfigurationTests.cs diff --git a/examples/Example.MinimalApi/appsettings.json b/examples/Example.MinimalApi/appsettings.json index c058ae2..9beeb3c 100644 --- a/examples/Example.MinimalApi/appsettings.json +++ b/examples/Example.MinimalApi/appsettings.json @@ -12,8 +12,8 @@ }, "Elastic": { "OpenTelemetry": { - "FileLogDirectory": "C:\\Logs\\OtelDistro", - "FileLogLevel": "Information" + "LogDirectory": "C:\\Logs\\OtelDistro", + "LogLevel": "Information" } } } diff --git a/src/Elastic.OpenTelemetry/Configuration/ElasticOpenTelemetryOptions.cs b/src/Elastic.OpenTelemetry/Configuration/ElasticOpenTelemetryOptions.cs index 3cf9d9e..f5f622e 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 System.Collections; using System.Runtime.InteropServices; using Elastic.OpenTelemetry.Diagnostics.Logging; using Microsoft.Extensions.Configuration; @@ -26,36 +27,46 @@ namespace Elastic.OpenTelemetry.Configuration; 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 LogDirectoryConfigPropertyName = "LogDirectory"; + private static readonly string LogLevelConfigPropertyName = "LogLevel"; + private static readonly string LogTargetsConfigPropertyName = "LogTargets"; 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 LogLevel? _fileLogLevel; - private ConfigSource _fileLogLevelSource = ConfigSource.Default; - private bool? _skipOtlpExporter; - private ConfigSource _skipOtlpExporterSource = ConfigSource.Default; - private string? _enabledElasticDefaults; - private ConfigSource _enabledElasticDefaultsSource = ConfigSource.Default; - private string? _loggingSectionLogLevel; + private string? _logDirectory; + private ConfigSource _logDirectorySource = ConfigSource.Default; + + private LogLevel? _logLevel; + private ConfigSource _logLevelSource = ConfigSource.Default; + + private LogTargets? _logTargets; + private ConfigSource _logTargetsSource = ConfigSource.Default; + + private readonly bool? _skipOtlpExporter; + private readonly ConfigSource _skipOtlpExporterSource = ConfigSource.Default; + + private readonly string? _enabledElasticDefaults; + private readonly ConfigSource _enabledElasticDefaultsSource = ConfigSource.Default; + private string? _loggingSectionLogLevel; private readonly string _defaultLogDirectory; + private readonly IDictionary _environmentVariables; /// /// Creates a new instance of with properties /// bound from environment variables. /// - public ElasticOpenTelemetryOptions() + public ElasticOpenTelemetryOptions(IDictionary? environmentVariables = null) { _defaultLogDirectory = GetDefaultLogDirectory(); - SetFromEnvironment(ELASTIC_OTEL_LOG_DIRECTORY, ref _fileLogDirectory, ref _fileLogDirectorySource, StringParser); - SetFromEnvironment(ELASTIC_OTEL_LOG_LEVEL, ref _fileLogLevel, ref _fileLogLevelSource, LogLevelParser); + _environmentVariables = environmentVariables ?? Environment.GetEnvironmentVariables(); + SetFromEnvironment(ELASTIC_OTEL_LOG_DIRECTORY, ref _logDirectory, ref _logDirectorySource, StringParser); + SetFromEnvironment(ELASTIC_OTEL_LOG_LEVEL, ref _logLevel, ref _logLevelSource, LogLevelParser); + SetFromEnvironment(ELASTIC_OTEL_LOG_TARGETS, ref _logTargets, ref _logTargetsSource, LogTargetsParser); SetFromEnvironment(ELASTIC_OTEL_SKIP_OTLP_EXPORTER, ref _skipOtlpExporter, ref _skipOtlpExporterSource, BoolParser); SetFromEnvironment(ELASTIC_OTEL_ENABLE_ELASTIC_DEFAULTS, ref _enabledElasticDefaults, ref _enabledElasticDefaultsSource, StringParser); } @@ -64,11 +75,13 @@ public ElasticOpenTelemetryOptions() /// Creates a new instance of with properties /// bound from environment variables and an instance. /// - internal ElasticOpenTelemetryOptions(IConfiguration? configuration) : this() + internal ElasticOpenTelemetryOptions(IConfiguration? configuration, IDictionary? environmentVariables = null) + : this(environmentVariables) { if (configuration is null) return; - SetFromConfiguration(configuration, FileLogDirectoryConfigPropertyName, ref _fileLogDirectory, ref _fileLogDirectorySource, StringParser); - SetFromConfiguration(configuration, FileLogLevelConfigPropertyName, ref _fileLogLevel, ref _fileLogLevelSource, LogLevelParser); + SetFromConfiguration(configuration, LogDirectoryConfigPropertyName, ref _logDirectory, ref _logDirectorySource, StringParser); + SetFromConfiguration(configuration, LogLevelConfigPropertyName, ref _logLevel, ref _logLevelSource, LogLevelParser); + SetFromConfiguration(configuration, LogTargetsConfigPropertyName, ref _logTargets, ref _logTargetsSource, LogTargetsParser); SetFromConfiguration(configuration, SkipOtlpExporterConfigPropertyName, ref _skipOtlpExporter, ref _skipOtlpExporterSource, BoolParser); SetFromConfiguration(configuration, EnabledElasticDefaultsConfigPropertyName, ref _enabledElasticDefaults, ref _enabledElasticDefaultsSource, StringParser); @@ -84,11 +97,31 @@ void BindFromLoggingSection(IConfiguration config) if (string.IsNullOrEmpty(_loggingSectionLogLevel)) _loggingSectionLogLevel = config.GetValue("Logging:LogLevel:Default"); - if (!string.IsNullOrEmpty(_loggingSectionLogLevel) && _fileLogLevel is null) + if (!string.IsNullOrEmpty(_loggingSectionLogLevel) && _logLevel is null) + { + _logLevel = LogLevelHelpers.ToLogLevel(_loggingSectionLogLevel); + _logLevelSource = ConfigSource.IConfiguration; + } + } + } + + /// + /// Calculates whether global logging is enabled based on + /// , and + /// + public bool GlobalLogEnabled + { + get + { + var isActive = (_logLevel.HasValue || !string.IsNullOrWhiteSpace(_logDirectory) || _logTargets.HasValue); + if (isActive) { - _fileLogLevel = LogLevelHelpers.ToLogLevel(_loggingSectionLogLevel); - _fileLogLevelSource = ConfigSource.IConfiguration; + if (_logLevel is LogLevel.None) + isActive = false; + else if (_logTargets is LogTargets.None) + isActive = false; } + return isActive; } } @@ -106,7 +139,7 @@ private static string GetDefaultLogDirectory() => /// - /var/log/elastic/apm-agent-dotnet (on Linux) /// - ~/Library/Application_Support/elastic/apm-agent-dotnet (on OSX) /// - public string FileLogDirectoryDefault => _defaultLogDirectory; + public string LogDirectoryDefault => _defaultLogDirectory ; /// /// The output directory where the Elastic distribution of OpenTelemetry will write log files. @@ -116,13 +149,13 @@ private static string GetDefaultLogDirectory() => /// {ProcessName}_{UtcUnixTimeMilliseconds}_{ProcessId}.instrumentation.log. /// This log file includes log messages from the OpenTelemetry SDK and the Elastic distribution. /// - public string? FileLogDirectory + public string LogDirectory { - get => _fileLogDirectory; + get => _logDirectory ?? LogDirectoryDefault; init { - _fileLogDirectory = value; - _fileLogDirectorySource = ConfigSource.Property; + _logDirectory = value; + _logDirectorySource = ConfigSource.Property; } } @@ -141,13 +174,24 @@ public string? FileLogDirectory /// TraceContain the most detailed messages. /// /// - public LogLevel? FileLogLevel + public LogLevel LogLevel { - get => _fileLogLevel; + get => _logLevel ?? LogLevel.Warning; init { - _fileLogLevel = value; - _fileLogLevelSource = ConfigSource.Property; + _logLevel = value; + _logLevelSource = ConfigSource.Property; + } + } + + /// > + public LogTargets LogTargets + { + get => _logTargets ?? (GlobalLogEnabled ? LogTargets.File : LogTargets.None); + init + { + _logTargets = value; + _logTargetsSource = ConfigSource.Property; } } @@ -194,13 +238,42 @@ public string EnableElasticDefaults private static (bool, LogLevel?) LogLevelParser(string? s) => !string.IsNullOrEmpty(s) ? (true, LogLevelHelpers.ToLogLevel(s)) : (false, null); + private static (bool, LogTargets?) LogTargetsParser(string? s) + { + //var tokens = s?.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries }); + if (string.IsNullOrWhiteSpace(s)) + return (false, null); + + var logTargets = LogTargets.None; + var found = false; + + foreach (var target in s.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries)) + { + if (IsSet(target, "stdout")) + logTargets |= LogTargets.StdOut; + else if (IsSet(target, "file")) + logTargets |= LogTargets.File; + else if (IsSet(target, "none")) + logTargets |= LogTargets.None; + } + return !found ? (false, null) : (true, logTargets); + + bool IsSet(string k, string v) + { + var b = k.Trim().Equals(v, StringComparison.InvariantCultureIgnoreCase); + if (b) + found = true; + return b; + } + } + 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) + private void SetFromEnvironment(string key, ref T field, ref ConfigSource configSourceField, Func parser) { - var (success, value) = parser(Environment.GetEnvironmentVariable(key)); + var (success, value) = parser(GetSafeEnvironmentVariable(key)); if (success) { @@ -264,13 +337,20 @@ private EnabledElasticDefaults GetEnabledElasticDefaults() static EnabledElasticDefaults All() => EnabledElasticDefaults.Tracing | EnabledElasticDefaults.Metrics | EnabledElasticDefaults.Logging; } + private string GetSafeEnvironmentVariable(string key) + { + var value = _environmentVariables.Contains(key) ? _environmentVariables[key]?.ToString() : null; + return value ?? string.Empty; + } + + 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}]", LogDirectoryConfigPropertyName, + _logDirectory, _logDirectorySource); - logger.LogInformation("Configured value for {ConfigKey}: '{ConfigValue}' from [{ConfigSource}]", FileLogLevelConfigPropertyName, - _fileLogLevel, _fileLogLevelSource); + logger.LogInformation("Configured value for {ConfigKey}: '{ConfigValue}' from [{ConfigSource}]", LogLevelConfigPropertyName, + _logLevel, _logLevelSource); logger.LogInformation("Configured value for {ConfigKey}: '{ConfigValue}' from [{ConfigSource}]", SkipOtlpExporterConfigPropertyName, _skipOtlpExporter, _skipOtlpExporterSource); diff --git a/src/Elastic.OpenTelemetry/Configuration/EnvironmentVariables.cs b/src/Elastic.OpenTelemetry/Configuration/EnvironmentVariables.cs index 63312a2..4417e5c 100644 --- a/src/Elastic.OpenTelemetry/Configuration/EnvironmentVariables.cs +++ b/src/Elastic.OpenTelemetry/Configuration/EnvironmentVariables.cs @@ -11,6 +11,7 @@ internal static class EnvironmentVariables public const string ELASTIC_OTEL_LOG_DIRECTORY = nameof(ELASTIC_OTEL_LOG_DIRECTORY); public const string ELASTIC_OTEL_LOG_LEVEL = nameof(ELASTIC_OTEL_LOG_LEVEL); + public const string ELASTIC_OTEL_LOG_TARGETS = nameof(ELASTIC_OTEL_LOG_TARGETS); public const string ELASTIC_OTEL_ENABLE_ELASTIC_DEFAULTS = nameof(ELASTIC_OTEL_ENABLE_ELASTIC_DEFAULTS); // ReSharper enable InconsistentNaming diff --git a/src/Elastic.OpenTelemetry/Configuration/LogTargets.cs b/src/Elastic.OpenTelemetry/Configuration/LogTargets.cs new file mode 100644 index 0000000..04ab903 --- /dev/null +++ b/src/Elastic.OpenTelemetry/Configuration/LogTargets.cs @@ -0,0 +1,26 @@ +// 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; + +/// +/// Control how the distribution should globally log. +/// +[Flags] +public enum LogTargets +{ + + /// No global logging + None, + /// + /// Enable file logging. Use + /// and to set any values other than the defaults + /// + File = 1 << 0, //1 + /// + /// Write to standard out, useful in scenarios where file logging might not be an option or harder to set up. + /// e.g. containers, k8s, etc. + /// + StdOut = 1 << 1, //2 +} diff --git a/src/Elastic.OpenTelemetry/Diagnostics/Logging/AgentLoggingHelpers.cs b/src/Elastic.OpenTelemetry/Diagnostics/Logging/AgentLoggingHelpers.cs index 666b66c..9fb96f7 100644 --- a/src/Elastic.OpenTelemetry/Diagnostics/Logging/AgentLoggingHelpers.cs +++ b/src/Elastic.OpenTelemetry/Diagnostics/Logging/AgentLoggingHelpers.cs @@ -3,28 +3,12 @@ // 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 DefaultLogLevel => LogLevel.Information; - - public static LogLevel GetElasticOtelLogLevelFromEnvironmentVariables() - { - var defaultLogLevel = DefaultLogLevel; - - var logLevelEnvironmentVariable = Environment.GetEnvironmentVariable(EnvironmentVariables.ELASTIC_OTEL_LOG_LEVEL); - - 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) { var state = new LogState diff --git a/src/Elastic.OpenTelemetry/Diagnostics/Logging/FileLogger.cs b/src/Elastic.OpenTelemetry/Diagnostics/Logging/FileLogger.cs index f0a56c4..927861f 100644 --- a/src/Elastic.OpenTelemetry/Diagnostics/Logging/FileLogger.cs +++ b/src/Elastic.OpenTelemetry/Diagnostics/Logging/FileLogger.cs @@ -32,18 +32,17 @@ internal sealed class FileLogger : IDisposable, IAsyncDisposable, ILogger public FileLogger(ElasticOpenTelemetryOptions options) { _scopeProvider = new LoggerExternalScopeProvider(); + _configuredLogLevel = options.LogLevel; + FileLoggingEnabled = options.GlobalLogEnabled; - var logDirectory = options.FileLogDirectory; - var logLevel = options.FileLogLevel; - if (logLevel == LogLevel.None || (logLevel == null && logDirectory == null)) + if (!FileLoggingEnabled) return; - _configuredLogLevel = logLevel ?? LogLevel.Information; - logDirectory ??= options.FileLogDirectoryDefault; - 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 var logFileName = $"{process.ProcessName}_{DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()}_{process.Id}.instrumentation.log"; + + var logDirectory = options.LogDirectory; LogFilePath = Path.Combine(logDirectory, logFileName); if (!Directory.Exists(logDirectory)) diff --git a/src/Elastic.OpenTelemetry/Diagnostics/Logging/LogLevelHelpers.cs b/src/Elastic.OpenTelemetry/Diagnostics/Logging/LogLevelHelpers.cs index a7db33f..82d43cd 100644 --- a/src/Elastic.OpenTelemetry/Diagnostics/Logging/LogLevelHelpers.cs +++ b/src/Elastic.OpenTelemetry/Diagnostics/Logging/LogLevelHelpers.cs @@ -16,32 +16,27 @@ internal static class LogLevelHelpers public const string Trace = "Trace"; public const string None = "None"; - public static LogLevel ToLogLevel(string logLevelString) + 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; + return LogLevel.Trace; + if (logLevelString.Equals(Debug, StringComparison.OrdinalIgnoreCase)) + return LogLevel.Debug; + if (logLevelString.Equals("Info", StringComparison.OrdinalIgnoreCase)) + return LogLevel.Information; + if (logLevelString.Equals(Information, StringComparison.OrdinalIgnoreCase)) + return LogLevel.Information; + if (logLevelString.Equals("Warn", StringComparison.OrdinalIgnoreCase)) + return LogLevel.Warning; + if (logLevelString.Equals(Warning, StringComparison.OrdinalIgnoreCase)) + return LogLevel.Warning; + if (logLevelString.Equals(Error, StringComparison.OrdinalIgnoreCase)) + return LogLevel.Error; + if (logLevelString.Equals(Critical, StringComparison.OrdinalIgnoreCase)) + return LogLevel.Critical; + if (logLevelString.Equals(None, StringComparison.OrdinalIgnoreCase)) + return LogLevel.None; + return null; } public static string AsString(this LogLevel logLevel) => diff --git a/src/Elastic.OpenTelemetry/Diagnostics/LoggingEventListener.cs b/src/Elastic.OpenTelemetry/Diagnostics/LoggingEventListener.cs index e62bfc0..9c52487 100644 --- a/src/Elastic.OpenTelemetry/Diagnostics/LoggingEventListener.cs +++ b/src/Elastic.OpenTelemetry/Diagnostics/LoggingEventListener.cs @@ -38,10 +38,10 @@ public LoggingEventListener(ILogger logger, ElasticOpenTelemetryOptions options) // 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 subscribe 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 = options.FileLogLevel; + var eventLevel = options.LogLevel; if (!string.IsNullOrEmpty(options.LoggingSectionLogLevel)) { - var logLevel = LogLevelHelpers.ToLogLevel(options.LoggingSectionLogLevel); + var logLevel = LogLevelHelpers.ToLogLevel(options.LoggingSectionLogLevel) ?? LogLevel.None; if (logLevel < eventLevel) eventLevel = logLevel; diff --git a/tests/Elastic.OpenTelemetry.Tests/Configuration/ElasticOpenTelemetryOptionsTests.cs b/tests/Elastic.OpenTelemetry.Tests/Configuration/ElasticOpenTelemetryOptionsTests.cs index b373c02..21490b1 100644 --- a/tests/Elastic.OpenTelemetry.Tests/Configuration/ElasticOpenTelemetryOptionsTests.cs +++ b/tests/Elastic.OpenTelemetry.Tests/Configuration/ElasticOpenTelemetryOptionsTests.cs @@ -2,26 +2,23 @@ // 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.Collections; using System.Text; using Elastic.OpenTelemetry.Configuration; using Elastic.OpenTelemetry.Extensions; using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; using OpenTelemetry; using Xunit.Abstractions; using static Elastic.OpenTelemetry.Configuration.ElasticOpenTelemetryOptions; +using static Elastic.OpenTelemetry.Configuration.EnvironmentVariables; using static Elastic.OpenTelemetry.Diagnostics.Logging.LogLevelHelpers; namespace Elastic.OpenTelemetry.Tests.Configuration; -public sealed class ElasticOpenTelemetryOptionsTests(ITestOutputHelper output) : IDisposable +public sealed class ElasticOpenTelemetryOptionsTests(ITestOutputHelper output) { - private readonly ITestOutputHelper _output = output; - private readonly string? _originalFileLogDirectoryEnvVar = Environment.GetEnvironmentVariable(EnvironmentVariables.ELASTIC_OTEL_LOG_DIRECTORY); - private readonly string? _originalFileLogLevelEnvVar = Environment.GetEnvironmentVariable(EnvironmentVariables.ELASTIC_OTEL_LOG_LEVEL); - private readonly string? _originalEnableElasticDefaultsEnvVar = Environment.GetEnvironmentVariable(EnvironmentVariables.ELASTIC_OTEL_ENABLE_ELASTIC_DEFAULTS); - private readonly string? _originalSkipOtlpExporterEnvVar = Environment.GetEnvironmentVariable(EnvironmentVariables.ELASTIC_OTEL_SKIP_OTLP_EXPORTER); - [Fact] public void EnabledElasticDefaults_NoneIncludesExpectedValues() { @@ -35,16 +32,18 @@ public void EnabledElasticDefaults_NoneIncludesExpectedValues() [Fact] public void DefaultCtor_SetsExpectedDefaults_WhenNoEnvironmentVariablesAreConfigured() { - Environment.SetEnvironmentVariable(EnvironmentVariables.ELASTIC_OTEL_LOG_DIRECTORY, null); - Environment.SetEnvironmentVariable(EnvironmentVariables.ELASTIC_OTEL_LOG_LEVEL, null); - Environment.SetEnvironmentVariable(EnvironmentVariables.ELASTIC_OTEL_ENABLE_ELASTIC_DEFAULTS, null); - Environment.SetEnvironmentVariable(EnvironmentVariables.ELASTIC_OTEL_SKIP_OTLP_EXPORTER, null); - - var sut = new ElasticOpenTelemetryOptions(); + var sut = new ElasticOpenTelemetryOptions(new Hashtable + { + {ELASTIC_OTEL_LOG_DIRECTORY, null}, + {ELASTIC_OTEL_LOG_LEVEL, null}, + {ELASTIC_OTEL_ENABLE_ELASTIC_DEFAULTS, null}, + {ELASTIC_OTEL_SKIP_OTLP_EXPORTER, null}, + }); + sut.GlobalLogEnabled.Should().Be(false); // these default to null because any other value would enable file logging - sut.FileLogDirectory.Should().Be(null); - sut.FileLogLevel.Should().Be(null); + sut.LogDirectory.Should().Be(sut.LogDirectoryDefault); + sut.LogLevel.Should().Be(LogLevel.Warning); sut.EnableElasticDefaults.Should().Be(string.Empty); sut.EnabledDefaults.Should().HaveFlag(EnabledElasticDefaults.Tracing); @@ -52,17 +51,13 @@ public void DefaultCtor_SetsExpectedDefaults_WhenNoEnvironmentVariablesAreConfig sut.EnabledDefaults.Should().HaveFlag(EnabledElasticDefaults.Logging); sut.SkipOtlpExporter.Should().Be(false); - var logger = new TestLogger(_output); + 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] @@ -72,30 +67,28 @@ public void DefaultCtor_LoadsConfigurationFromEnvironmentVariables() const string fileLogLevel = "Critical"; const string enabledElasticDefaults = "None"; - Environment.SetEnvironmentVariable(EnvironmentVariables.ELASTIC_OTEL_LOG_DIRECTORY, fileLogDirectory); - Environment.SetEnvironmentVariable(EnvironmentVariables.ELASTIC_OTEL_LOG_LEVEL, fileLogLevel); - Environment.SetEnvironmentVariable(EnvironmentVariables.ELASTIC_OTEL_ENABLE_ELASTIC_DEFAULTS, enabledElasticDefaults); - Environment.SetEnvironmentVariable(EnvironmentVariables.ELASTIC_OTEL_SKIP_OTLP_EXPORTER, "true"); - - var sut = new ElasticOpenTelemetryOptions(); - - sut.FileLogDirectory.Should().Be(fileLogDirectory); - sut.FileLogLevel.Should().Be(ToLogLevel(fileLogLevel)); + var sut = new ElasticOpenTelemetryOptions(new Hashtable + { + {ELASTIC_OTEL_LOG_DIRECTORY, fileLogDirectory}, + {ELASTIC_OTEL_LOG_LEVEL, fileLogLevel}, + {ELASTIC_OTEL_ENABLE_ELASTIC_DEFAULTS, enabledElasticDefaults}, + {ELASTIC_OTEL_SKIP_OTLP_EXPORTER, "true"}, + }); + + sut.LogDirectory.Should().Be(fileLogDirectory); + sut.LogLevel.Should().Be(ToLogLevel(fileLogLevel)); sut.EnableElasticDefaults.Should().Be(enabledElasticDefaults); sut.EnabledDefaults.Should().Be(EnabledElasticDefaults.None); sut.SkipOtlpExporter.Should().Be(true); - var logger = new TestLogger(_output); + 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] @@ -105,12 +98,6 @@ public void ConfigurationCtor_LoadsConfigurationFromIConfiguration() const string fileLogLevel = "Critical"; const string enabledElasticDefaults = "None"; - // Remove all env vars - Environment.SetEnvironmentVariable(EnvironmentVariables.ELASTIC_OTEL_LOG_DIRECTORY, null); - Environment.SetEnvironmentVariable(EnvironmentVariables.ELASTIC_OTEL_LOG_LEVEL, null); - Environment.SetEnvironmentVariable(EnvironmentVariables.ELASTIC_OTEL_ENABLE_ELASTIC_DEFAULTS, null); - Environment.SetEnvironmentVariable(EnvironmentVariables.ELASTIC_OTEL_SKIP_OTLP_EXPORTER, null); - var json = $$""" { "Logging": { @@ -121,8 +108,8 @@ public void ConfigurationCtor_LoadsConfigurationFromIConfiguration() }, "Elastic": { "OpenTelemetry": { - "FileLogDirectory": "C:\\Temp", - "FileLogLevel": "{{fileLogLevel}}", + "LogDirectory": "C:\\Temp", + "LogLevel": "{{fileLogLevel}}", "EnabledElasticDefaults": "{{enabledElasticDefaults}}", "SkipOtlpExporter": true } @@ -134,26 +121,22 @@ public void ConfigurationCtor_LoadsConfigurationFromIConfiguration() .AddJsonStream(new MemoryStream(Encoding.UTF8.GetBytes(json))) .Build(); - var sut = new ElasticOpenTelemetryOptions(config); + var sut = new ElasticOpenTelemetryOptions(config, new Hashtable()); - sut.FileLogDirectory.Should().Be(@"C:\Temp"); - sut.FileLogLevel.Should().Be(ToLogLevel(fileLogLevel)); + sut.LogDirectory.Should().Be(@"C:\Temp"); + sut.LogLevel.Should().Be(ToLogLevel(fileLogLevel)); 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); + 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] @@ -162,12 +145,6 @@ public void ConfigurationCtor_LoadsConfigurationFromIConfiguration_AndFallsBackT const string loggingSectionLogLevel = "Warning"; const string enabledElasticDefaults = "None"; - // Remove all env vars - Environment.SetEnvironmentVariable(EnvironmentVariables.ELASTIC_OTEL_LOG_DIRECTORY, null); - Environment.SetEnvironmentVariable(EnvironmentVariables.ELASTIC_OTEL_LOG_LEVEL, null); - Environment.SetEnvironmentVariable(EnvironmentVariables.ELASTIC_OTEL_ENABLE_ELASTIC_DEFAULTS, null); - Environment.SetEnvironmentVariable(EnvironmentVariables.ELASTIC_OTEL_SKIP_OTLP_EXPORTER, null); - var json = $$""" { "Logging": { @@ -178,7 +155,7 @@ public void ConfigurationCtor_LoadsConfigurationFromIConfiguration_AndFallsBackT }, "Elastic": { "OpenTelemetry": { - "FileLogDirectory": "C:\\Temp", + "LogDirectory": "C:\\Temp", "EnabledElasticDefaults": "{{enabledElasticDefaults}}", "SkipOtlpExporter": true } @@ -190,26 +167,23 @@ public void ConfigurationCtor_LoadsConfigurationFromIConfiguration_AndFallsBackT .AddJsonStream(new MemoryStream(Encoding.UTF8.GetBytes(json))) .Build(); - var sut = new ElasticOpenTelemetryOptions(config); + var sut = new ElasticOpenTelemetryOptions(config, new Hashtable()); - sut.FileLogDirectory.Should().Be(@"C:\Temp"); - sut.FileLogLevel.Should().Be(ToLogLevel(loggingSectionLogLevel)); + sut.LogDirectory.Should().Be(@"C:\Temp"); + sut.LogLevel.Should().Be(ToLogLevel(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); + 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] @@ -218,12 +192,6 @@ public void ConfigurationCtor_LoadsConfigurationFromIConfiguration_AndFallsBackT const string loggingSectionDefaultLogLevel = "Information"; const string enabledElasticDefaults = "None"; - // Remove all env vars - Environment.SetEnvironmentVariable(EnvironmentVariables.ELASTIC_OTEL_LOG_DIRECTORY, null); - Environment.SetEnvironmentVariable(EnvironmentVariables.ELASTIC_OTEL_LOG_LEVEL, null); - Environment.SetEnvironmentVariable(EnvironmentVariables.ELASTIC_OTEL_ENABLE_ELASTIC_DEFAULTS, null); - Environment.SetEnvironmentVariable(EnvironmentVariables.ELASTIC_OTEL_SKIP_OTLP_EXPORTER, null); - var json = $$""" { "Logging": { @@ -233,7 +201,7 @@ public void ConfigurationCtor_LoadsConfigurationFromIConfiguration_AndFallsBackT }, "Elastic": { "OpenTelemetry": { - "FileLogDirectory": "C:\\Temp", + "LogDirectory": "C:\\Temp", "EnabledElasticDefaults": "{{enabledElasticDefaults}}", "SkipOtlpExporter": true } @@ -245,26 +213,21 @@ public void ConfigurationCtor_LoadsConfigurationFromIConfiguration_AndFallsBackT .AddJsonStream(new MemoryStream(Encoding.UTF8.GetBytes(json))) .Build(); - var sut = new ElasticOpenTelemetryOptions(config); + var sut = new ElasticOpenTelemetryOptions(config, new Hashtable()); - sut.FileLogDirectory.Should().Be(@"C:\Temp"); - sut.FileLogLevel.Should().Be(ToLogLevel(loggingSectionDefaultLogLevel)); + sut.LogDirectory.Should().Be(@"C:\Temp"); + sut.LogLevel.Should().Be(ToLogLevel(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); + 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(); + foreach (var message in logger.Messages) message.Should().EndWith("from [IConfiguration]"); } [Fact] @@ -274,17 +237,12 @@ public void EnvironmentVariables_TakePrecedenceOver_ConfigValues() const string fileLogLevel = "Critical"; const string enabledElasticDefaults = "None"; - Environment.SetEnvironmentVariable(EnvironmentVariables.ELASTIC_OTEL_LOG_DIRECTORY, fileLogDirectory); - Environment.SetEnvironmentVariable(EnvironmentVariables.ELASTIC_OTEL_LOG_LEVEL, fileLogLevel); - Environment.SetEnvironmentVariable(EnvironmentVariables.ELASTIC_OTEL_ENABLE_ELASTIC_DEFAULTS, enabledElasticDefaults); - Environment.SetEnvironmentVariable(EnvironmentVariables.ELASTIC_OTEL_SKIP_OTLP_EXPORTER, "true"); - var json = $$""" { "Elastic": { "OpenTelemetry": { - "FileLogDirectory": "C:\\Json", - "FileLogLevel": "Trace", + "LogDirectory": "C:\\Json", + "LogLevel": "Trace", "EnabledElasticDefaults": "All", "SkipOtlpExporter": false } @@ -296,15 +254,19 @@ public void EnvironmentVariables_TakePrecedenceOver_ConfigValues() .AddJsonStream(new MemoryStream(Encoding.UTF8.GetBytes(json))) .Build(); - var sut = new ElasticOpenTelemetryOptions(config); - - sut.FileLogDirectory.Should().Be(fileLogDirectory); - sut.FileLogLevel.Should().Be(ToLogLevel(fileLogLevel)); + var sut = new ElasticOpenTelemetryOptions(config, new Hashtable + { + {ELASTIC_OTEL_LOG_DIRECTORY, fileLogDirectory}, + {ELASTIC_OTEL_LOG_LEVEL, fileLogLevel}, + {ELASTIC_OTEL_ENABLE_ELASTIC_DEFAULTS, enabledElasticDefaults}, + {ELASTIC_OTEL_SKIP_OTLP_EXPORTER, "true"}, + }); + + sut.LogDirectory.Should().Be(fileLogDirectory); + sut.LogLevel.Should().Be(ToLogLevel(fileLogLevel)); sut.EnableElasticDefaults.Should().Be(enabledElasticDefaults); sut.EnabledDefaults.Should().Be(EnabledElasticDefaults.None); sut.SkipOtlpExporter.Should().Be(true); - - ResetEnvironmentVariables(); } [Fact] @@ -314,36 +276,33 @@ public void InitializedProperties_TakePrecedenceOver_EnvironmentValues() const string fileLogLevel = "Critical"; const string enabledElasticDefaults = "None"; - Environment.SetEnvironmentVariable(EnvironmentVariables.ELASTIC_OTEL_LOG_DIRECTORY, "C:\\Temp"); - Environment.SetEnvironmentVariable(EnvironmentVariables.ELASTIC_OTEL_LOG_LEVEL, "Information"); - Environment.SetEnvironmentVariable(EnvironmentVariables.ELASTIC_OTEL_ENABLE_ELASTIC_DEFAULTS, "All"); - Environment.SetEnvironmentVariable(EnvironmentVariables.ELASTIC_OTEL_SKIP_OTLP_EXPORTER, "true"); - - var sut = new ElasticOpenTelemetryOptions + var sut = new ElasticOpenTelemetryOptions(new Hashtable + { + {ELASTIC_OTEL_LOG_DIRECTORY, "C:\\Temp"}, + {ELASTIC_OTEL_LOG_LEVEL, "Information"}, + {ELASTIC_OTEL_ENABLE_ELASTIC_DEFAULTS, "All"}, + {ELASTIC_OTEL_SKIP_OTLP_EXPORTER, "true"}, + }) { - FileLogDirectory = fileLogDirectory, - FileLogLevel = ToLogLevel(fileLogLevel), + LogDirectory = fileLogDirectory, + LogLevel = ToLogLevel(fileLogLevel) ?? LogLevel.None, SkipOtlpExporter = false, EnableElasticDefaults = enabledElasticDefaults }; - sut.FileLogDirectory.Should().Be(fileLogDirectory); - sut.FileLogLevel.Should().Be(ToLogLevel(fileLogLevel)); + sut.LogDirectory.Should().Be(fileLogDirectory); + sut.LogLevel.Should().Be(ToLogLevel(fileLogLevel)); sut.EnableElasticDefaults.Should().Be(enabledElasticDefaults); sut.EnabledDefaults.Should().Be(EnabledElasticDefaults.None); sut.SkipOtlpExporter.Should().Be(false); - var logger = new TestLogger(_output); + 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] @@ -433,7 +392,7 @@ public void TransactionId_IsNotAdded_WhenElasticDefaultsDoesNotIncludeTracing() { var options = new ElasticOpenTelemetryBuilderOptions { - Logger = new TestLogger(_output), + Logger = new TestLogger(output), DistroOptions = new ElasticOpenTelemetryOptions() { SkipOtlpExporter = true, @@ -468,14 +427,4 @@ public void TransactionId_IsNotAdded_WhenElasticDefaultsDoesNotIncludeTracing() transactionId.Should().BeNull(); } - - private void ResetEnvironmentVariables() - { - Environment.SetEnvironmentVariable(EnvironmentVariables.ELASTIC_OTEL_LOG_DIRECTORY, _originalFileLogDirectoryEnvVar); - Environment.SetEnvironmentVariable(EnvironmentVariables.ELASTIC_OTEL_LOG_LEVEL, _originalFileLogLevelEnvVar); - Environment.SetEnvironmentVariable(EnvironmentVariables.ELASTIC_OTEL_ENABLE_ELASTIC_DEFAULTS, _originalEnableElasticDefaultsEnvVar); - Environment.SetEnvironmentVariable(EnvironmentVariables.ELASTIC_OTEL_SKIP_OTLP_EXPORTER, _originalSkipOtlpExporterEnvVar); - } - - public void Dispose() => ResetEnvironmentVariables(); } diff --git a/tests/Elastic.OpenTelemetry.Tests/Configuration/GlobalLogConfigurationTests.cs b/tests/Elastic.OpenTelemetry.Tests/Configuration/GlobalLogConfigurationTests.cs new file mode 100644 index 0000000..4fc5e42 --- /dev/null +++ b/tests/Elastic.OpenTelemetry.Tests/Configuration/GlobalLogConfigurationTests.cs @@ -0,0 +1,147 @@ +// 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.Collections; +using Elastic.OpenTelemetry.Configuration; +using Microsoft.Extensions.Logging; +using static Elastic.OpenTelemetry.Configuration.EnvironmentVariables; + +namespace Elastic.OpenTelemetry.Tests.Configuration; + +public class GlobalLogConfigurationTests +{ + [Fact] + public void Check_Defaults() + { + var config = new ElasticOpenTelemetryOptions(new Hashtable()); + config.GlobalLogEnabled.Should().BeFalse(); + config.LogLevel.Should().Be(LogLevel.Warning); + config.LogDirectory.Should().Be(config.LogDirectoryDefault); + config.LogTargets.Should().Be(LogTargets.None); + } + + // + [Theory] + [InlineData(ELASTIC_OTEL_LOG_LEVEL, "Info")] + [InlineData(ELASTIC_OTEL_LOG_DIRECTORY, "1")] + //only if explicitly specified to 'none' should we not default to file logging. + [InlineData(ELASTIC_OTEL_LOG_TARGETS, "file")] + public void CheckActivation(string environmentVariable, string value) + { + var config = new ElasticOpenTelemetryOptions(new Hashtable { { environmentVariable, value } }); + config.GlobalLogEnabled.Should().BeTrue(); + config.LogTargets.Should().Be(LogTargets.File); + } + + // + [Theory] + [InlineData(ELASTIC_OTEL_LOG_LEVEL, "none")] + //only if explicitly specified to 'none' should we not default to file logging. + [InlineData(ELASTIC_OTEL_LOG_TARGETS, "none")] + public void CheckDeactivation(string environmentVariable, string value) + { + var config = new ElasticOpenTelemetryOptions(new Hashtable + { + { ELASTIC_OTEL_LOG_DIRECTORY, "" }, + { environmentVariable, value } + }); + config.GlobalLogEnabled.Should().BeFalse(); + config.LogTargets.Should().Be(LogTargets.None); + } + + [Theory] + //only specifying apm_log_level not sufficient, needs explicit directory configuration + //setting targets to none will result in no global trace logging + [InlineData(ELASTIC_OTEL_LOG_TARGETS, "None")] + //setting file log level to none will result in no global trace logging + [InlineData(ELASTIC_OTEL_LOG_LEVEL, "None")] + public void CheckNonActivation(string environmentVariable, string value) + { + var config = new ElasticOpenTelemetryOptions(new Hashtable { { environmentVariable, value } }); + config.GlobalLogEnabled.Should().BeFalse(); + } + + [Theory] + [InlineData("trace", LogLevel.Trace)] + [InlineData("Trace", LogLevel.Trace)] + [InlineData("TraCe", LogLevel.Trace)] + [InlineData("debug", LogLevel.Debug)] + [InlineData("info", LogLevel.Information)] + [InlineData("warn", LogLevel.Warning)] + [InlineData("error", LogLevel.Error)] + [InlineData("none", LogLevel.None)] + public void Check_LogLevelValues_AreMappedCorrectly(string envVarValue, LogLevel logLevel) + { + Check(ELASTIC_OTEL_LOG_LEVEL, envVarValue, logLevel); + return; + + static void Check(string key, string envVarValue, LogLevel level) + { + var config = CreateConfig(key, envVarValue); + config.LogLevel.Should().Be(level, "{0}", key); + } + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData("foo")] + [InlineData("tracing")] + public void Check_InvalidLogLevelValues_AreMappedToDefaultWarn(string? envVarValue) + { + Check(ELASTIC_OTEL_LOG_LEVEL, envVarValue); + return; + + static void Check(string key, string? envVarValue) + { + var config = CreateConfig(key, envVarValue); + config.LogLevel.Should().Be(LogLevel.Warning, "{0}", key); + } + } + + [Fact] + public void Check_LogDir_IsEvaluatedCorrectly() + { + Check(ELASTIC_OTEL_LOG_DIRECTORY, "/foo/bar"); + return; + + static void Check(string key, string envVarValue) + { + var config = CreateConfig(key, envVarValue); + config.LogDirectory.Should().StartWith("/foo/bar", "{0}", key); + } + } + + [Theory] + [InlineData(null, LogTargets.None)] + [InlineData("", LogTargets.None)] + [InlineData("foo", LogTargets.None)] + [InlineData("foo,bar", LogTargets.None)] + [InlineData("foo;bar", LogTargets.None)] + [InlineData("file;foo;bar", LogTargets.File)] + [InlineData("file", LogTargets.File)] + [InlineData("stdout", LogTargets.StdOut)] + [InlineData("StdOut", LogTargets.StdOut)] + [InlineData("file;stdout", LogTargets.File | LogTargets.StdOut)] + [InlineData("FILE;StdOut", LogTargets.File | LogTargets.StdOut)] + [InlineData("file;stdout;file", LogTargets.File | LogTargets.StdOut)] + [InlineData("FILE;StdOut;stdout", LogTargets.File | LogTargets.StdOut)] + internal void Check_LogTargets_AreEvaluatedCorrectly(string? envVarValue, LogTargets? targets) + { + Check(ELASTIC_OTEL_LOG_TARGETS, envVarValue, targets); + return; + + static void Check(string key, string? envVarValue, LogTargets? targets) + { + var config = CreateConfig(key, envVarValue); + config.LogTargets.Should().Be(targets, "{0}", key); + } + } + + private static ElasticOpenTelemetryOptions CreateConfig(string key, string? envVarValue) + { + var environment = new Hashtable { { key, envVarValue } }; + return new ElasticOpenTelemetryOptions(environment); + } +}