From 3d093c9450ceb921fdc9e006c12196f7f862377b Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Thu, 18 Jul 2024 13:07:17 +0200 Subject: [PATCH] Ensure we respect OTEL instrumentation/signal configuration (#131) * simplify our defaults enablement a tad * Ensure we listen to OTEL_*_INSTRUMENTATION_ENABLED * move parsers into their own files and started on infra for enabled instrumentations * Add env and config parsers to clean up Options * cleanup * dotnet format * EnabledDefaults => ElasticDefaults * ELASTIC_OTEL_ENABLE_ELASTIC_DEFAULTS => ELASTIC_OTEL_DEFAULTS_ENABLE * ENABLE => ENABLED * EnabledSignals => Signals --- docs/configure.mdx | 8 +- .../Configuration/ConfigCell.cs | 28 ++ .../Configuration/ElasticDefaults.cs | 29 ++ .../ElasticOpenTelemetryOptions.cs | 337 ++++++---------- .../Configuration/EnvironmentVariables.cs | 13 +- .../Instrumentations/LogInstrumentation.cs | 38 ++ .../Instrumentations/MetricInstrumentation.cs | 47 +++ .../Instrumentations/TraceInstrumentation.cs | 100 +++++ .../Parsers/ConfigurationParser.cs | 168 ++++++++ .../Parsers/EnvironmentParser.cs | 105 +++++ .../Configuration/Parsers/SharedParsers.cs | 132 +++++++ .../Configuration/Signals.cs | 28 ++ .../Diagnostics/LoggingEventListener.cs | 22 +- .../Elastic.OpenTelemetry.csproj | 1 + .../ElasticOpenTelemetryBuilder.cs | 54 ++- .../MeterProviderBuilderExtensions.cs | 3 +- .../OpenTelemetryLoggerOptionsExtensions.cs | 3 +- .../TracerProviderBuilderExtensions.cs | 3 +- .../ElasticOpenTelemetryOptionsTests.cs | 363 ++++++------------ .../EnabledDefaultsConfigurationTests.cs | 124 ++++++ .../EnabledSignalsConfigurationTests.cs | 248 ++++++++++++ .../Processors/TransactionProcessorTests.cs | 52 +++ 22 files changed, 1399 insertions(+), 507 deletions(-) create mode 100644 src/Elastic.OpenTelemetry/Configuration/ConfigCell.cs create mode 100644 src/Elastic.OpenTelemetry/Configuration/ElasticDefaults.cs create mode 100644 src/Elastic.OpenTelemetry/Configuration/Instrumentations/LogInstrumentation.cs create mode 100644 src/Elastic.OpenTelemetry/Configuration/Instrumentations/MetricInstrumentation.cs create mode 100644 src/Elastic.OpenTelemetry/Configuration/Instrumentations/TraceInstrumentation.cs create mode 100644 src/Elastic.OpenTelemetry/Configuration/Parsers/ConfigurationParser.cs create mode 100644 src/Elastic.OpenTelemetry/Configuration/Parsers/EnvironmentParser.cs create mode 100644 src/Elastic.OpenTelemetry/Configuration/Parsers/SharedParsers.cs create mode 100644 src/Elastic.OpenTelemetry/Configuration/Signals.cs create mode 100644 tests/Elastic.OpenTelemetry.Tests/Configuration/EnabledDefaultsConfigurationTests.cs create mode 100644 tests/Elastic.OpenTelemetry.Tests/Configuration/EnabledSignalsConfigurationTests.cs create mode 100644 tests/Elastic.OpenTelemetry.Tests/Processors/TransactionProcessorTests.cs diff --git a/docs/configure.mdx b/docs/configure.mdx index a9491bc..ab6befe 100644 --- a/docs/configure.mdx +++ b/docs/configure.mdx @@ -141,12 +141,12 @@ an OTLP endpoint. This can be useful when you want to test applications without [float] [[config-enabledelasticdefaults]] -### `EnabledElasticDefaults` +### `ElasticDefaults` A comma-separated list of Elastic defaults to enable. This can be useful when you want to enable only some of the Elastic Distribution for OpenTelemetry .NET opinionated defaults. -Valid options: `None`, `Tracing`, `Metrics`, `Logging`. +Valid options: `None`, `Traces`, `Metrics`, `Logs`, `All`. Except for the `None` option, all other options can be combined. @@ -159,11 +159,11 @@ OpenTelemetry SDK configuration. You may then choose to configure the various pr as required. In all other cases, the Elastic Distribution for OpenTelemetry .NET will enable the specified defaults. For example, to enable only -Elastic defaults only for tracing and metrics, set this value to `Tracing,Metrics`. +Elastic defaults only for tracing and metrics, set this value to `Traces,Metrics`. | Environment variable name | IConfiguration key | | ------------- |-------------| -| `ELASTIC_OTEL_ENABLE_ELASTIC_DEFAULTS` | `Elastic:OpenTelemetry:EnabledElasticDefaults` | +| `ELASTIC_OTEL_DEFAULTS_ENABLED` | `Elastic:OpenTelemetry:ElasticDefaults` | | Default | Type | | ------------- |-------------| diff --git a/src/Elastic.OpenTelemetry/Configuration/ConfigCell.cs b/src/Elastic.OpenTelemetry/Configuration/ConfigCell.cs new file mode 100644 index 0000000..f140cfd --- /dev/null +++ b/src/Elastic.OpenTelemetry/Configuration/ConfigCell.cs @@ -0,0 +1,28 @@ +// 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 class ConfigCell(string key, T value) +{ + public string Key { get; } = key; + public T? Value { get; private set; } = value; + public ConfigSource Source { get; set; } = ConfigSource.Default; + + public void Assign(T value, ConfigSource source) + { + Value = value; + Source = source; + } + + public override string ToString() => $"{Key}: '{Value}' from [{Source}]"; +} +internal enum ConfigSource +{ + Default, // Default value assigned within this class + Environment, // Loaded from an environment variable + // ReSharper disable once InconsistentNaming + IConfiguration, // Bound from an IConfiguration instance + Property // Set via property initializer +} diff --git a/src/Elastic.OpenTelemetry/Configuration/ElasticDefaults.cs b/src/Elastic.OpenTelemetry/Configuration/ElasticDefaults.cs new file mode 100644 index 0000000..38f0ed7 --- /dev/null +++ b/src/Elastic.OpenTelemetry/Configuration/ElasticDefaults.cs @@ -0,0 +1,29 @@ +// 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 which elastic defaults you want to include. +/// NOTE: this is an expert level option only use this if you want to take full control of the OTEL configuration +/// defaults to +/// +[Flags] +public enum ElasticDefaults +{ + /// No Elastic defaults will be included, acting effectively as a vanilla OpenTelemetry + None, + + /// Include Elastic Distribution for OpenTelemetry .NET tracing defaults + Traces = 1 << 0, //1 + + /// Include Elastic Distribution for OpenTelemetry .NET metrics defaults + Metrics = 1 << 1, //2 + + /// Include Elastic Distribution for OpenTelemetry .NET logging defaults + Logs = 1 << 2, //4 + + /// (default) Include all Elastic Distribution for OpenTelemetry .NET logging defaults + All = ~0 +} diff --git a/src/Elastic.OpenTelemetry/Configuration/ElasticOpenTelemetryOptions.cs b/src/Elastic.OpenTelemetry/Configuration/ElasticOpenTelemetryOptions.cs index 714fa36..3d47cc1 100644 --- a/src/Elastic.OpenTelemetry/Configuration/ElasticOpenTelemetryOptions.cs +++ b/src/Elastic.OpenTelemetry/Configuration/ElasticOpenTelemetryOptions.cs @@ -3,14 +3,17 @@ // See the LICENSE file in the project root for more information using System.Collections; -using System.Diagnostics; +using System.Diagnostics.Tracing; using System.Runtime.InteropServices; -using Elastic.OpenTelemetry.Diagnostics.Logging; +using Elastic.OpenTelemetry.Configuration.Instrumentations; +using Elastic.OpenTelemetry.Configuration.Parsers; +using Elastic.OpenTelemetry.Diagnostics; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using static System.Environment; using static System.Runtime.InteropServices.RuntimeInformation; using static Elastic.OpenTelemetry.Configuration.EnvironmentVariables; +using static Elastic.OpenTelemetry.Configuration.Parsers.SharedParsers; namespace Elastic.OpenTelemetry.Configuration; @@ -29,37 +32,20 @@ namespace Elastic.OpenTelemetry.Configuration; /// public class ElasticOpenTelemetryOptions { - private static readonly string ConfigurationSection = "Elastic:OpenTelemetry"; - 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"; + private readonly ConfigCell _logDirectory = new(nameof(LogDirectory), null); + private readonly ConfigCell _logTargets = new(nameof(LogTargets), null); - // 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 readonly EventLevel _eventLevel = EventLevel.Informational; + private readonly ConfigCell _logLevel = new(nameof(LogLevel), LogLevel.Warning); + private readonly ConfigCell _skipOtlpExporter = new(nameof(SkipOtlpExporter), false); + private readonly ConfigCell _enabledDefaults = new(nameof(ElasticDefaults), ElasticDefaults.All); + private readonly ConfigCell _runningInContainer = new(nameof(_runningInContainer), false); + private readonly ConfigCell _signals = new(nameof(Signals), Signals.All); - private string? _logDirectory; - private ConfigSource _logDirectorySource = ConfigSource.Default; + private readonly ConfigCell _tracing = new(nameof(Tracing), TraceInstrumentations.All); + private readonly ConfigCell _metrics = new(nameof(Metrics), MetricInstrumentations.All); + private readonly ConfigCell _logging = new(nameof(Logging), LogInstrumentations.All); - 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 readonly bool? _runningInContainer; - private readonly ConfigSource _runningInContainerSource = ConfigSource.Default; - - private string? _loggingSectionLogLevel; - private readonly string _defaultLogDirectory; private readonly IDictionary _environmentVariables; /// @@ -68,15 +54,19 @@ public class ElasticOpenTelemetryOptions /// public ElasticOpenTelemetryOptions(IDictionary? environmentVariables = null) { - _defaultLogDirectory = GetDefaultLogDirectory(); + LogDirectoryDefault = GetDefaultLogDirectory(); _environmentVariables = environmentVariables ?? GetEnvironmentVariables(); - SetFromEnvironment(DOTNET_RUNNING_IN_CONTAINER, ref _runningInContainer, ref _runningInContainerSource, BoolParser); + SetFromEnvironment(DOTNET_RUNNING_IN_CONTAINER, _runningInContainer, BoolParser); + + SetFromEnvironment(OTEL_DOTNET_AUTO_LOG_DIRECTORY, _logDirectory, StringParser); + SetFromEnvironment(OTEL_LOG_LEVEL, _logLevel, LogLevelParser); + SetFromEnvironment(ELASTIC_OTEL_LOG_TARGETS, _logTargets, LogTargetsParser); + SetFromEnvironment(ELASTIC_OTEL_SKIP_OTLP_EXPORTER, _skipOtlpExporter, BoolParser); + SetFromEnvironment(ELASTIC_OTEL_DEFAULTS_ENABLED, _enabledDefaults, ElasticDefaultsParser); + + var parser = new EnvironmentParser(_environmentVariables); + parser.ParseInstrumentationVariables(_signals, _tracing, _metrics, _logging); - SetFromEnvironment(OTEL_DOTNET_AUTO_LOG_DIRECTORY, ref _logDirectory, ref _logDirectorySource, StringParser); - SetFromEnvironment(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); } /// @@ -88,30 +78,17 @@ internal ElasticOpenTelemetryOptions(IConfiguration? configuration, IDictionary? { if (configuration is null) return; - 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); - BindFromLoggingSection(configuration); + var parser = new ConfigurationParser(configuration); + parser.ParseLogDirectory(_logDirectory); + parser.ParseLogTargets(_logTargets); + parser.ParseLogLevel(_logLevel, ref _eventLevel); + parser.ParseSkipOtlpExporter(_skipOtlpExporter); + parser.ParseElasticDefaults(_enabledDefaults); + parser.ParseSignals(_signals); + + parser.ParseInstrumentations(_tracing, _metrics, _logging); - void BindFromLoggingSection(IConfiguration config) - { - // 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 = config.GetValue($"Logging:LogLevel:{CompositeLogger.LogCategory}"); - - // Fall back to the default logging level if the specific category is not configured. - if (string.IsNullOrEmpty(_loggingSectionLogLevel)) - _loggingSectionLogLevel = config.GetValue("Logging:LogLevel:Default"); - - if (!string.IsNullOrEmpty(_loggingSectionLogLevel) && _logLevel is null) - { - _logLevel = LogLevelHelpers.ToLogLevel(_loggingSectionLogLevel); - _logLevelSource = ConfigSource.IConfiguration; - } - } } /// @@ -122,13 +99,15 @@ public bool GlobalLogEnabled { get { - var isActive = _logLevel is <= LogLevel.Debug || !string.IsNullOrWhiteSpace(_logDirectory) || _logTargets.HasValue; + var level = _logLevel.Value; + var targets = _logTargets.Value; + var isActive = level is <= LogLevel.Debug || !string.IsNullOrWhiteSpace(_logDirectory.Value) || targets.HasValue; if (!isActive) return isActive; - if (_logLevel is LogLevel.None) + if (level is LogLevel.None) isActive = false; - else if (_logTargets is LogTargets.None) + else if (targets is LogTargets.None) isActive = false; return isActive; } @@ -141,6 +120,7 @@ private static string GetDefaultLogDirectory() return Path.Combine(GetFolderPath(SpecialFolder.ApplicationData), "elastic", applicationMoniker); if (IsOSPlatform(OSPlatform.OSX)) return Path.Combine(GetFolderPath(SpecialFolder.LocalApplicationData), "elastic", applicationMoniker); + return $"/var/log/elastic/{applicationMoniker}"; } @@ -151,7 +131,7 @@ private static string GetDefaultLogDirectory() /// - /var/log/elastic/apm-agent-dotnet (on Linux) /// - ~/Library/Application_Support/elastic/apm-agent-dotnet (on OSX) /// - public string LogDirectoryDefault => _defaultLogDirectory; + public string LogDirectoryDefault { get; } /// /// The output directory where the Elastic Distribution for OpenTelemetry .NET will write log files. @@ -163,14 +143,15 @@ private static string GetDefaultLogDirectory() /// public string LogDirectory { - get => _logDirectory ?? LogDirectoryDefault; - init - { - _logDirectory = value; - _logDirectorySource = ConfigSource.Property; - } + get => _logDirectory.Value ?? LogDirectoryDefault; + init => _logDirectory.Assign(value, ConfigSource.Property); } + /// + /// Used by to determine the appropiate event level to subscribe to + /// + internal EventLevel EventLogLevel => _eventLevel; + /// /// The log level to use when writing log files. /// @@ -188,25 +169,17 @@ public string LogDirectory /// public LogLevel LogLevel { - get => _logLevel ?? LogLevel.Warning; - init - { - _logLevel = value; - _logLevelSource = ConfigSource.Property; - } + get => _logLevel.Value ?? LogLevel.Warning; + init => _logLevel.Assign(value, ConfigSource.Property); } - /// > + /// public LogTargets LogTargets { - get => _logTargets ?? (GlobalLogEnabled ? - _runningInContainer.HasValue && _runningInContainer.Value ? LogTargets.StdOut : LogTargets.File + get => _logTargets.Value ?? (GlobalLogEnabled + ? _runningInContainer.Value.HasValue && _runningInContainer.Value.Value ? LogTargets.StdOut : LogTargets.File : LogTargets.None); - init - { - _logTargets = value; - _logTargetsSource = ConfigSource.Property; - } + init => _logTargets.Assign(value, ConfigSource.Property); } /// @@ -214,147 +187,77 @@ public LogTargets LogTargets /// public bool SkipOtlpExporter { - get => _skipOtlpExporter ?? false; - init - { - _skipOtlpExporter = value; - _skipOtlpExporterSource = ConfigSource.Property; - } + get => _skipOtlpExporter.Value ?? false; + init => _skipOtlpExporter.Assign(value, ConfigSource.Property); } /// - /// A comma separated list of instrumentation signal Elastic defaults. + /// Allows flags to be set based of to selectively opt in to Elastic Distribution for OpenTelemetry .NET features. + /// Defaults to /// /// /// 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. + /// None Disables all Elastic defaults resulting in the use of the "vanilla" SDK. + /// All Enables all defaults (default if this option is not specified). + /// Tracing Enables Elastic defaults for tracing. + /// Metrics Enables Elastic defaults for metrics. + /// Logging Enables Elastic defaults for logging. /// /// - public string EnableElasticDefaults + public ElasticDefaults ElasticDefaults { - get => _enabledElasticDefaults ?? string.Empty; - init - { - _enabledElasticDefaults = value; - _enabledElasticDefaultsSource = ConfigSource.Property; - } + get => _enabledDefaults.Value ?? ElasticDefaults.All; + init => _enabledDefaults.Assign(value, ConfigSource.Property); } - internal string? LoggingSectionLogLevel => _loggingSectionLogLevel; - - internal EnabledElasticDefaults EnabledDefaults => _elasticDefaults ?? GetEnabledElasticDefaults(); - - private static (bool, LogLevel?) LogLevelParser(string? s) => - !string.IsNullOrEmpty(s) ? (true, LogLevelHelpers.ToLogLevel(s)) : (false, null); - - private static (bool, LogTargets?) LogTargetsParser(string? s) + /// + /// Control which signals will be automatically enabled by the Elastic Distribution for OpenTelemetry .NET. + /// + /// This configuration respects the open telemetry environment configuration out of the box: + /// + /// + /// + /// + /// + /// + /// Setting this propery in code or configuration will take precedence over environment variables + /// + public Signals Signals { - //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; - } + get => _signals.Value ?? Signals.All; + init => _signals.Assign(value, ConfigSource.Property); } - private static (bool, string) StringParser(string? s) => !string.IsNullOrEmpty(s) ? (true, s) : (false, string.Empty); - - private static (bool, bool?) BoolParser(string? s) => - s switch - { - "1" => (true, true), - "0" => (true, false), - _ => bool.TryParse(s, out var boolValue) ? (true, boolValue) : (false, null) - }; - - private void SetFromEnvironment(string key, ref T field, ref ConfigSource configSourceField, Func parser) + /// Enabled trace instrumentations + public TraceInstrumentations Tracing { - var (success, value) = parser(GetSafeEnvironmentVariable(key)); - - if (success) - { - field = value; - configSourceField = ConfigSource.Environment; - } + get => _tracing.Value ?? TraceInstrumentations.All; + init => _tracing.Assign(value, ConfigSource.Property); } - private static void SetFromConfiguration(IConfiguration configuration, string key, ref T field, ref ConfigSource configSourceField, - Func parser) + /// Enabled trace instrumentations + public MetricInstrumentations Metrics { - if (field is null) - { - var logFileDirectory = configuration.GetValue($"{ConfigurationSection}:{key}"); - - var (success, value) = parser(logFileDirectory); - - if (success) - { - field = value; - configSourceField = ConfigSource.IConfiguration; - } - } + get => _metrics.Value ?? MetricInstrumentations.All; + init => _metrics.Assign(value, ConfigSource.Property); } - private EnabledElasticDefaults GetEnabledElasticDefaults() + /// Enabled trace instrumentations + public LogInstrumentations Logging { - 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(); + get => _logging.Value ?? LogInstrumentations.All; + init => _logging.Assign(value, ConfigSource.Property); + } - _elasticDefaults = defaults; + private void SetFromEnvironment(string key, ConfigCell field, Func parser) + { + var value = parser(GetSafeEnvironmentVariable(key)); + if (value is null) + return; - return defaults; + field.Assign(value, ConfigSource.Environment); - static EnabledElasticDefaults All() => EnabledElasticDefaults.Tracing | EnabledElasticDefaults.Metrics | EnabledElasticDefaults.Logging; } private string GetSafeEnvironmentVariable(string key) @@ -366,33 +269,13 @@ private string GetSafeEnvironmentVariable(string key) internal void LogConfigSources(ILogger logger) { - logger.LogInformation("Configured value for {ConfigKey}: '{ConfigValue}' from [{ConfigSource}]", LogDirectoryConfigPropertyName, - _logDirectory, _logDirectorySource); - - logger.LogInformation("Configured value for {ConfigKey}: '{ConfigValue}' from [{ConfigSource}]", LogLevelConfigPropertyName, - _logLevel, _logLevelSource); - - 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 + logger.LogInformation("Configured value for {Configuration}", _logDirectory); + logger.LogInformation("Configured value for {Configuration}", _logLevel); + logger.LogInformation("Configured value for {Configuration}", _skipOtlpExporter); + logger.LogInformation("Configured value for {Configuration}", _enabledDefaults); + logger.LogInformation("Configured value for {Configuration}", _signals); + logger.LogInformation("Configured value for {Configuration}", _tracing); + logger.LogInformation("Configured value for {Configuration}", _metrics); + logger.LogInformation("Configured value for {Configuration}", _logging); } } diff --git a/src/Elastic.OpenTelemetry/Configuration/EnvironmentVariables.cs b/src/Elastic.OpenTelemetry/Configuration/EnvironmentVariables.cs index 6827fa8..61a6aea 100644 --- a/src/Elastic.OpenTelemetry/Configuration/EnvironmentVariables.cs +++ b/src/Elastic.OpenTelemetry/Configuration/EnvironmentVariables.cs @@ -7,15 +7,24 @@ namespace Elastic.OpenTelemetry.Configuration; internal static class EnvironmentVariables { // ReSharper disable InconsistentNaming + // ReSharper disable IdentifierTypo public const string ELASTIC_OTEL_SKIP_OTLP_EXPORTER = nameof(ELASTIC_OTEL_SKIP_OTLP_EXPORTER); public const string OTEL_DOTNET_AUTO_LOG_DIRECTORY = nameof(OTEL_DOTNET_AUTO_LOG_DIRECTORY); public const string OTEL_LOG_LEVEL = nameof(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); public const string DOTNET_RUNNING_IN_CONTAINER = nameof(DOTNET_RUNNING_IN_CONTAINER); + + public const string ELASTIC_OTEL_DEFAULTS_ENABLED = nameof(ELASTIC_OTEL_DEFAULTS_ENABLED); + + public const string OTEL_DOTNET_AUTO_INSTRUMENTATION_ENABLED = nameof(OTEL_DOTNET_AUTO_INSTRUMENTATION_ENABLED); + + public const string OTEL_DOTNET_AUTO_TRACES_INSTRUMENTATION_ENABLED = nameof(OTEL_DOTNET_AUTO_TRACES_INSTRUMENTATION_ENABLED); + public const string OTEL_DOTNET_AUTO_METRICS_INSTRUMENTATION_ENABLED = nameof(OTEL_DOTNET_AUTO_METRICS_INSTRUMENTATION_ENABLED); + public const string OTEL_DOTNET_AUTO_LOGS_INSTRUMENTATION_ENABLED = nameof(OTEL_DOTNET_AUTO_LOGS_INSTRUMENTATION_ENABLED); + + // ReSharper enable IdentifierTypo // ReSharper enable InconsistentNaming } diff --git a/src/Elastic.OpenTelemetry/Configuration/Instrumentations/LogInstrumentation.cs b/src/Elastic.OpenTelemetry/Configuration/Instrumentations/LogInstrumentation.cs new file mode 100644 index 0000000..b2c0240 --- /dev/null +++ b/src/Elastic.OpenTelemetry/Configuration/Instrumentations/LogInstrumentation.cs @@ -0,0 +1,38 @@ +// 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 NetEscapades.EnumGenerators; + +namespace Elastic.OpenTelemetry.Configuration.Instrumentations; + +/// A hash set to enable +public class LogInstrumentations : HashSet +{ + /// All available + public static readonly LogInstrumentations All = new([.. LogInstrumentationExtensions.GetValues()]); + + /// Explicitly enable specific + public LogInstrumentations(IEnumerable instrumentations) : base(instrumentations) { } + + /// + public override string ToString() + { + if (Count == 0) + return "None"; + if (Count == All.Count) + return "All"; + if (All.Count - Count < 5) + return $"All Except: {string.Join(", ", All.Except(this).Select(i => i.ToStringFast()))}"; + return string.Join(", ", this.Select(i => i.ToStringFast())); + } +} + +/// Available logs instrumentations. +[EnumExtensions] +public enum LogInstrumentation +{ + /// ILogger instrumentation + // ReSharper disable once InconsistentNaming + ILogger +} diff --git a/src/Elastic.OpenTelemetry/Configuration/Instrumentations/MetricInstrumentation.cs b/src/Elastic.OpenTelemetry/Configuration/Instrumentations/MetricInstrumentation.cs new file mode 100644 index 0000000..1bc5dbf --- /dev/null +++ b/src/Elastic.OpenTelemetry/Configuration/Instrumentations/MetricInstrumentation.cs @@ -0,0 +1,47 @@ +// 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 NetEscapades.EnumGenerators; + +namespace Elastic.OpenTelemetry.Configuration.Instrumentations; + +/// A hash set to enable +public class MetricInstrumentations : HashSet +{ + /// All available + public static readonly MetricInstrumentations All = new([.. MetricInstrumentationExtensions.GetValues()]); + + /// Explicitly enable specific + public MetricInstrumentations(IEnumerable instrumentations) : base(instrumentations) { } + + /// + public override string ToString() + { + if (Count == 0) + return "None"; + if (Count == All.Count) + return "All"; + if (All.Count - Count < 5) + return $"All Except: {string.Join(", ", All.Except(this).Select(i => i.ToStringFast()))}"; + return string.Join(", ", this.Select(i => i.ToStringFast())); + } +} + +/// Available metric instrumentations. +[EnumExtensions] +public enum MetricInstrumentation +{ + ///ASP.NET Framework + AspNet, + ///ASP.NET Core + AspNetCore, + ///System.Net.Http.HttpClient and System.Net.HttpWebRequest, HttpClient metrics + HttpClient, + ///OpenTelemetry.Instrumentation.Runtime, Runtime metrics + NetRuntime, + ///OpenTelemetry.Instrumentation.Process,Process metrics + Process, + ///NServiceBus metrics + NServiceBus +} diff --git a/src/Elastic.OpenTelemetry/Configuration/Instrumentations/TraceInstrumentation.cs b/src/Elastic.OpenTelemetry/Configuration/Instrumentations/TraceInstrumentation.cs new file mode 100644 index 0000000..eaca173 --- /dev/null +++ b/src/Elastic.OpenTelemetry/Configuration/Instrumentations/TraceInstrumentation.cs @@ -0,0 +1,100 @@ +// 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 NetEscapades.EnumGenerators; + +namespace Elastic.OpenTelemetry.Configuration.Instrumentations; + +/// A hash set to enable +public class TraceInstrumentations : HashSet +{ + /// All available + public static readonly TraceInstrumentations All = new([.. TraceInstrumentationExtensions.GetValues()]); + + /// Explicitly enable specific + public TraceInstrumentations(IEnumerable instrumentations) : base(instrumentations) { } + + /// + public override string ToString() + { + if (Count == 0) + return "None"; + if (Count == All.Count) + return "All"; + if (All.Count - Count < 5) + return $"All Except: {string.Join(", ", All.Except(this).Select(i => i.ToStringFast()))}"; + return string.Join(", ", this.Select(i => i.ToStringFast())); + } +} + +/// Available trace instrumentations. +[EnumExtensions] +public enum TraceInstrumentation +{ + ///ASP.NET (.NET Framework) MVC / WebApi + AspNet, + + ///ASP.NET Core + AspNetCore, + + ///Azure SDK + Azure, + + ///Elastic.Clients.Elasticsearch + Elasticsearch, + + ///Elastic.Transport >=0.4.16 + ElasticTransport, + + ///Microsoft.EntityFrameworkCore Not supported on.NET Framework >=6.0.12 + EntityFrameworkCore, + + ///GraphQL Not supported on.NET Framework >=7.5.0 + Graphql, + + ///Grpc.Net.Client >=2.52 .0 & < 3.0.0 + GrpcNetClient, + + ///System.Net.Http.HttpClient and System.Net.HttpWebRequest + HttpClient, + + ///Confluent.Kafka >=1.4 .0 & < 3.0.0 + Kafka, + + ///MassTransit Not supported on.NET Framework ≥8.0.0 + MassTransit, + + ///MongoDB.Driver.Core >=2.13 .3 & < 3.0.0 + MongoDb, + + ///MySqlConnector >=2.0.0 + MysqlConnector, + + ///MySql.Data Not supported on.NET Framework >=8.1.0 + MysqlData, + + ///Npgsql >=6.0.0 + Npgsql, + + ///NServiceBus >=8.0.0 & < 10.0.0 + NServiceBus, + + ///Oracle.ManagedDataAccess.Core and Oracle.ManagedDataAccess Not supported on ARM64 >=23.4.0 + OracleMda, + + ///Quartz Not supported on.NET Framework 4.7.1 and older >=3.4.0 + Quartz, + + ///Microsoft.Data.SqlClient, System.Data.SqlClient and System.Data (shipped with.NET Framework) + SqlClient, + + ///StackExchange.Redis Not supported on.NET Framework >=2.0.405 & < 3.0.0 + StackExchangeRedis, + + ///WCF + WcfClient, + + ///WCF Not supported on.NET. + WcfService +} diff --git a/src/Elastic.OpenTelemetry/Configuration/Parsers/ConfigurationParser.cs b/src/Elastic.OpenTelemetry/Configuration/Parsers/ConfigurationParser.cs new file mode 100644 index 0000000..2d359dd --- /dev/null +++ b/src/Elastic.OpenTelemetry/Configuration/Parsers/ConfigurationParser.cs @@ -0,0 +1,168 @@ +// 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.Diagnostics.Tracing; +using Elastic.OpenTelemetry.Configuration.Instrumentations; +using Elastic.OpenTelemetry.Diagnostics.Logging; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using static System.StringSplitOptions; +using static Elastic.OpenTelemetry.Configuration.Parsers.SharedParsers; + +namespace Elastic.OpenTelemetry.Configuration.Parsers; + +internal class ConfigurationParser +{ + private readonly IConfiguration _configuration; + private static readonly string ConfigurationSection = "Elastic:OpenTelemetry"; + + internal string? LoggingSectionLogLevel { get; } + + public ConfigurationParser(IConfiguration configuration) + { + _configuration = 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"); + } + + + private static void SetFromConfiguration(IConfiguration configuration, ConfigCell cell, Func parser) + { + //environment configuration takes precedence, assume already configured + if (cell.Source == ConfigSource.Environment) + return; + + var lookup = configuration.GetValue($"{ConfigurationSection}:{cell.Key}"); + if (lookup is null) + return; + var parsed = parser(lookup); + if (parsed is null) + return; + + cell.Assign(parsed, ConfigSource.IConfiguration); + } + + public void ParseLogDirectory(ConfigCell logDirectory) => + SetFromConfiguration(_configuration, logDirectory, StringParser); + + public void ParseLogTargets(ConfigCell logTargets) => + SetFromConfiguration(_configuration, logTargets, LogTargetsParser); + + public void ParseLogLevel(ConfigCell logLevel, ref EventLevel eventLevel) + { + SetFromConfiguration(_configuration, logLevel, LogLevelParser); + + if (!string.IsNullOrEmpty(LoggingSectionLogLevel) && logLevel.Source == ConfigSource.Default) + { + var level = LogLevelHelpers.ToLogLevel(LoggingSectionLogLevel); + logLevel.Assign(level, ConfigSource.IConfiguration); + } + + // this is used to ensure LoggingEventListener matches our log level by using the lowest + // of our configured loglevel or the default logging section's level. + var eventLogLevel = logLevel.Value; + if (!string.IsNullOrEmpty(LoggingSectionLogLevel)) + { + var sectionLogLevel = LogLevelHelpers.ToLogLevel(LoggingSectionLogLevel) ?? LogLevel.None; + + if (sectionLogLevel < eventLogLevel) + eventLogLevel = sectionLogLevel; + } + eventLevel = eventLogLevel switch + { + LogLevel.Trace => EventLevel.Verbose, + LogLevel.Information => EventLevel.Informational, + LogLevel.Warning => EventLevel.Warning, + LogLevel.Error => EventLevel.Error, + LogLevel.Critical => EventLevel.Critical, + _ => EventLevel.Informational // fallback to info level + }; + + } + + public void ParseSkipOtlpExporter(ConfigCell skipOtlpExporter) => + SetFromConfiguration(_configuration, skipOtlpExporter, BoolParser); + + public void ParseSignals(ConfigCell signals) => + SetFromConfiguration(_configuration, signals, SignalsParser); + + public void ParseElasticDefaults(ConfigCell defaults) => + SetFromConfiguration(_configuration, defaults, ElasticDefaultsParser); + + public void ParseInstrumentations( + ConfigCell tracing, + ConfigCell metrics, + ConfigCell logging + ) + { + if (tracing.Source != ConfigSource.Environment) + SetFromConfiguration(_configuration, tracing, ParseTracing); + + if (metrics.Source != ConfigSource.Environment) + SetFromConfiguration(_configuration, metrics, ParseMetrics); + + if (logging.Source != ConfigSource.Environment) + SetFromConfiguration(_configuration, logging, ParseLogs); + + } + + private static IEnumerable? ParseInstrumentation(string? config, T[] all, Func getter) + where T : struct + { + if (string.IsNullOrWhiteSpace(config)) + return null; + + var toRemove = new HashSet(); + var toAdd = new HashSet(); + + foreach (var token in config.Split(new[] { ';', ',' }, RemoveEmptyEntries)) + { + var candidate = token.Trim(); + var remove = candidate.StartsWith("-"); + candidate = candidate.TrimStart('-'); + + var instrumentation = getter(candidate); + if (!instrumentation.HasValue) + continue; + + if (remove) + toRemove.Add(instrumentation.Value); + else + toAdd.Add(instrumentation.Value); + } + if (toAdd.Count > 0) + return toAdd; + if (toRemove.Count > 0) + return all.Except(toRemove); + return null; + + } + + private static TraceInstrumentations? ParseTracing(string? tracing) + { + var instrumentations = ParseInstrumentation(tracing, TraceInstrumentationExtensions.GetValues(), + s => TraceInstrumentationExtensions.TryParse(s, out var instrumentation) ? instrumentation : null); + return instrumentations != null ? new TraceInstrumentations(instrumentations) : null; + } + + private static MetricInstrumentations? ParseMetrics(string? metrics) + { + var instrumentations = ParseInstrumentation(metrics, MetricInstrumentationExtensions.GetValues(), + s => MetricInstrumentationExtensions.TryParse(s, out var instrumentation) ? instrumentation : null); + return instrumentations != null ? new MetricInstrumentations(instrumentations) : null; + } + + private static LogInstrumentations? ParseLogs(string? logs) + { + var instrumentations = ParseInstrumentation(logs, LogInstrumentationExtensions.GetValues(), + s => LogInstrumentationExtensions.TryParse(s, out var instrumentation) ? instrumentation : null); + return instrumentations != null ? new LogInstrumentations(instrumentations) : null; + } +} diff --git a/src/Elastic.OpenTelemetry/Configuration/Parsers/EnvironmentParser.cs b/src/Elastic.OpenTelemetry/Configuration/Parsers/EnvironmentParser.cs new file mode 100644 index 0000000..b9d491c --- /dev/null +++ b/src/Elastic.OpenTelemetry/Configuration/Parsers/EnvironmentParser.cs @@ -0,0 +1,105 @@ +// 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.Instrumentations; +using static Elastic.OpenTelemetry.Configuration.EnvironmentVariables; +using static Elastic.OpenTelemetry.Configuration.Parsers.SharedParsers; + +namespace Elastic.OpenTelemetry.Configuration.Parsers; + +internal class EnvironmentParser(IDictionary environmentVariables) +{ + private string GetSafeEnvironmentVariable(string key) + { + var value = environmentVariables.Contains(key) ? environmentVariables[key]?.ToString() : null; + return value ?? string.Empty; + } + + internal (bool, HashSet) EnumerateEnabled(bool allEnabled, T[] available, Signals signal, Func getter) + { + var instrumentations = new HashSet(); + var signalEnv = signal.ToStringFast().ToUpperInvariant(); + var opted = false; + foreach (var instrumentation in available) + { + var name = getter(instrumentation).ToUpperInvariant(); + var key = $"OTEL_DOTNET_AUTO_{signalEnv}_{name}_INSTRUMENTATION_ENABLED"; + var enabled = BoolParser(GetSafeEnvironmentVariable(key)); + if ((enabled.HasValue && enabled.Value) || (!enabled.HasValue && allEnabled)) + instrumentations.Add(instrumentation); + if (enabled.HasValue) + opted = true; + } + return (opted, instrumentations); + + } + + internal (bool, HashSet) EnabledTraceInstrumentations(bool allEnabled) => + EnumerateEnabled(allEnabled, TraceInstrumentationExtensions.GetValues(), Signals.Traces, i => i.ToStringFast()); + + internal (bool, HashSet) EnabledMetricInstrumentations(bool allEnabled) => + EnumerateEnabled(allEnabled, MetricInstrumentationExtensions.GetValues(), Signals.Metrics, i => i.ToStringFast()); + + internal (bool, HashSet) EnabledLogInstrumentations(bool allEnabled) => + EnumerateEnabled(allEnabled, LogInstrumentationExtensions.GetValues(), Signals.Logs, i => i.ToStringFast()); + + public void ParseInstrumentationVariables( + ConfigCell signalsCell, + ConfigCell tracingCell, + ConfigCell metricsCell, + ConfigCell loggingCell + ) + { + var allEnabled = BoolParser(GetSafeEnvironmentVariable(OTEL_DOTNET_AUTO_INSTRUMENTATION_ENABLED)); + var defaultSignals = allEnabled.HasValue + ? allEnabled.Value ? Signals.All : Signals.None + : Signals.All; + + var logs = BoolParser(GetSafeEnvironmentVariable(OTEL_DOTNET_AUTO_LOGS_INSTRUMENTATION_ENABLED)); + var traces = BoolParser(GetSafeEnvironmentVariable(OTEL_DOTNET_AUTO_TRACES_INSTRUMENTATION_ENABLED)); + var metrics = BoolParser(GetSafeEnvironmentVariable(OTEL_DOTNET_AUTO_METRICS_INSTRUMENTATION_ENABLED)); + // was explicitly configured using environment variables + bool Configured(bool? source) => source ?? allEnabled ?? true; + + var traceEnabled = Configured(traces); + var (optedTraces, traceInstrumentations) = EnabledTraceInstrumentations(traceEnabled); + if (optedTraces) + tracingCell.Assign(new TraceInstrumentations(traceInstrumentations), ConfigSource.Environment); + + + var metricEnabled = Configured(metrics); + var (optedMetrics, metricInstrumentations) = EnabledMetricInstrumentations(metricEnabled); + if (optedMetrics) + metricsCell.Assign(new MetricInstrumentations(metricInstrumentations), ConfigSource.Environment); + + var logEnabled = Configured(logs); + var (optedLogs, logInstrumentations) = EnabledLogInstrumentations(logEnabled); + if (optedLogs) + loggingCell.Assign(new LogInstrumentations(logInstrumentations), ConfigSource.Environment); + + var signals = defaultSignals; + + if (logInstrumentations.Count > 0) + signals |= Signals.Logs; + else + signals &= ~Signals.Logs; + + if (traceInstrumentations.Count > 0) + signals |= Signals.Traces; + else + signals &= ~Signals.Traces; + + if (metricInstrumentations.Count > 0) + signals |= Signals.Metrics; + else + signals &= ~Signals.Metrics; + + if (logs.HasValue || traces.HasValue || traces.HasValue || allEnabled.HasValue) + signalsCell.Assign(signals, ConfigSource.Environment); + if (optedLogs || optedMetrics || optedTraces) + signalsCell.Assign(signals, ConfigSource.Environment); + + } +} diff --git a/src/Elastic.OpenTelemetry/Configuration/Parsers/SharedParsers.cs b/src/Elastic.OpenTelemetry/Configuration/Parsers/SharedParsers.cs new file mode 100644 index 0000000..ce40226 --- /dev/null +++ b/src/Elastic.OpenTelemetry/Configuration/Parsers/SharedParsers.cs @@ -0,0 +1,132 @@ +// 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 Elastic.OpenTelemetry.Diagnostics.Logging; +using Microsoft.Extensions.Logging; +using static System.StringComparison; +using static System.StringSplitOptions; + +namespace Elastic.OpenTelemetry.Configuration.Parsers; + +internal static class SharedParsers +{ + + internal static LogLevel? LogLevelParser(string? s) => + !string.IsNullOrEmpty(s) ? LogLevelHelpers.ToLogLevel(s) : null; + + internal static LogTargets? LogTargetsParser(string? s) + { + //var tokens = s?.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries }); + if (string.IsNullOrWhiteSpace(s)) + return null; + + var logTargets = LogTargets.None; + var found = false; + + foreach (var target in s.Split(new[] { ';', ',' }, 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 ? null : logTargets; + + bool IsSet(string k, string v) + { + var b = k.Trim().Equals(v, InvariantCultureIgnoreCase); + if (b) + found = true; + return b; + } + } + + internal static ElasticDefaults? ElasticDefaultsParser(string? s) + { + if (string.IsNullOrWhiteSpace(s)) + return null; + + var enabledDefaults = ElasticDefaults.None; + var found = false; + + foreach (var target in s.Split(new[] { ';', ',' }, RemoveEmptyEntries)) + { + if (IsSet(target, nameof(ElasticDefaults.Traces))) + enabledDefaults |= ElasticDefaults.Traces; + else if (IsSet(target, nameof(ElasticDefaults.Metrics))) + enabledDefaults |= ElasticDefaults.Metrics; + else if (IsSet(target, nameof(ElasticDefaults.Logs))) + enabledDefaults |= ElasticDefaults.Logs; + else if (IsSet(target, nameof(ElasticDefaults.All))) + { + enabledDefaults = ElasticDefaults.All; + break; + } + else if (IsSet(target, "none")) + { + enabledDefaults = ElasticDefaults.None; + break; + } + } + return !found ? null : enabledDefaults; + + bool IsSet(string k, string v) + { + var b = k.Trim().Equals(v, InvariantCultureIgnoreCase); + if (b) + found = true; + return b; + } + } + + internal static Signals? SignalsParser(string? s) + { + if (string.IsNullOrWhiteSpace(s)) + return null; + + var enabledDefaults = Signals.None; + var found = false; + + foreach (var target in s.Split(new[] { ';', ',' }, RemoveEmptyEntries)) + { + if (IsSet(target, nameof(Signals.Traces))) + enabledDefaults |= Signals.Traces; + else if (IsSet(target, nameof(Signals.Metrics))) + enabledDefaults |= Signals.Metrics; + else if (IsSet(target, nameof(Signals.Logs))) + enabledDefaults |= Signals.Logs; + else if (IsSet(target, nameof(Signals.All))) + { + enabledDefaults = Signals.All; + break; + } + else if (IsSet(target, "none")) + { + enabledDefaults = Signals.None; + break; + } + } + return !found ? null : enabledDefaults; + + bool IsSet(string k, string v) + { + var b = k.Trim().Equals(v, InvariantCultureIgnoreCase); + if (b) + found = true; + return b; + } + } + + internal static string? StringParser(string? s) => !string.IsNullOrEmpty(s) ? s : null; + + internal static bool? BoolParser(string? s) => + s switch + { + "1" => true, + "0" => false, + _ => bool.TryParse(s, out var boolValue) ? boolValue : null + }; +} diff --git a/src/Elastic.OpenTelemetry/Configuration/Signals.cs b/src/Elastic.OpenTelemetry/Configuration/Signals.cs new file mode 100644 index 0000000..3e78e16 --- /dev/null +++ b/src/Elastic.OpenTelemetry/Configuration/Signals.cs @@ -0,0 +1,28 @@ +// 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 NetEscapades.EnumGenerators; + +namespace Elastic.OpenTelemetry.Configuration; + +/// Observability signals to enable, defaults to all. +[Flags] +[EnumExtensions] +public enum Signals +{ + /// No Elastic defaults will be included, acting effectively as a vanilla OpenTelemetry + None, + + /// Include Elastic Distribution for OpenTelemetry .NET tracing defaults + Traces = 1 << 0, //1 + + /// Include Elastic Distribution for OpenTelemetry .NET metrics defaults + Metrics = 1 << 1, //2 + + /// Include Elastic Distribution for OpenTelemetry .NET logging defaults + Logs = 1 << 2, //4 + + /// (default) Include all Elastic Distribution for OpenTelemetry .NET logging defaults + All = ~0 +} diff --git a/src/Elastic.OpenTelemetry/Diagnostics/LoggingEventListener.cs b/src/Elastic.OpenTelemetry/Diagnostics/LoggingEventListener.cs index 9c52487..3eeed6f 100644 --- a/src/Elastic.OpenTelemetry/Diagnostics/LoggingEventListener.cs +++ b/src/Elastic.OpenTelemetry/Diagnostics/LoggingEventListener.cs @@ -34,28 +34,8 @@ class LoggingEventListener : EventListener, IAsyncDisposable public LoggingEventListener(ILogger logger, ElasticOpenTelemetryOptions options) { _logger = logger; + _eventLevel = options.EventLogLevel; - // 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.LogLevel; - if (!string.IsNullOrEmpty(options.LoggingSectionLogLevel)) - { - var logLevel = LogLevelHelpers.ToLogLevel(options.LoggingSectionLogLevel) ?? LogLevel.None; - - if (logLevel < eventLevel) - eventLevel = logLevel; - } - - _eventLevel = eventLevel switch - { - LogLevel.Trace => EventLevel.Verbose, - LogLevel.Information => EventLevel.Informational, - LogLevel.Warning => EventLevel.Warning, - LogLevel.Error => EventLevel.Error, - LogLevel.Critical => EventLevel.Critical, - _ => EventLevel.Informational // fallback to info level - }; } public override void Dispose() diff --git a/src/Elastic.OpenTelemetry/Elastic.OpenTelemetry.csproj b/src/Elastic.OpenTelemetry/Elastic.OpenTelemetry.csproj index 3bc3371..d16d0e7 100644 --- a/src/Elastic.OpenTelemetry/Elastic.OpenTelemetry.csproj +++ b/src/Elastic.OpenTelemetry/Elastic.OpenTelemetry.csproj @@ -28,6 +28,7 @@ + diff --git a/src/Elastic.OpenTelemetry/ElasticOpenTelemetryBuilder.cs b/src/Elastic.OpenTelemetry/ElasticOpenTelemetryBuilder.cs index 858dc44..217f973 100644 --- a/src/Elastic.OpenTelemetry/ElasticOpenTelemetryBuilder.cs +++ b/src/Elastic.OpenTelemetry/ElasticOpenTelemetryBuilder.cs @@ -4,6 +4,7 @@ using System.Diagnostics; using System.Diagnostics.Tracing; +using System.Reflection; using Elastic.OpenTelemetry.Configuration; using Elastic.OpenTelemetry.Diagnostics; using Elastic.OpenTelemetry.Diagnostics.Logging; @@ -17,6 +18,7 @@ using OpenTelemetry.Logs; using OpenTelemetry.Metrics; using OpenTelemetry.Trace; +using static Elastic.OpenTelemetry.Configuration.ElasticOpenTelemetryOptions; namespace Elastic.OpenTelemetry; @@ -98,7 +100,7 @@ public ElasticOpenTelemetryBuilder(ElasticOpenTelemetryBuilderOptions options) // We always add this so we can identify a distro is being used, even if all Elastic defaults are disabled. openTelemetry.ConfigureResource(r => r.UseElasticDefaults()); - if (options.DistroOptions.EnabledDefaults.Equals(ElasticOpenTelemetryOptions.EnabledElasticDefaults.None)) + if (options.DistroOptions.ElasticDefaults.Equals(ElasticDefaults.None)) { Logger.LogNoElasticDefaults(); @@ -108,22 +110,51 @@ public ElasticOpenTelemetryBuilder(ElasticOpenTelemetryBuilderOptions options) } openTelemetry.ConfigureResource(r => r.UseElasticDefaults(Logger)); + var distro = options.DistroOptions; //https://github.com/open-telemetry/opentelemetry-dotnet/pull/5400 - if (!options.DistroOptions.SkipOtlpExporter) + if (!distro.SkipOtlpExporter) openTelemetry.UseOtlpExporter(); - if (options.DistroOptions.EnabledDefaults.HasFlag(ElasticOpenTelemetryOptions.EnabledElasticDefaults.Logging)) + if (distro.Signals.HasFlag(Signals.Logs)) { //TODO Move to WithLogging once it gets stable - Services.Configure(logging => logging.UseElasticDefaults()); + Services.Configure(logging => + { + if (distro.ElasticDefaults.HasFlag(ElasticDefaults.Logs)) + logging.UseElasticDefaults(); + else + Logger.LogDefaultsDisabled(nameof(ElasticDefaults.Logs)); + }); } + else + Logger.LogSignalDisabled(nameof(Signals.Logs)); - if (options.DistroOptions.EnabledDefaults.HasFlag(ElasticOpenTelemetryOptions.EnabledElasticDefaults.Tracing)) - openTelemetry.WithTracing(tracing => tracing.UseElasticDefaults(Logger)); + if (distro.Signals.HasFlag(Signals.Traces)) + { + openTelemetry.WithTracing(tracing => + { + if (distro.ElasticDefaults.HasFlag(ElasticDefaults.Traces)) + tracing.UseElasticDefaults(Logger); + else + Logger.LogDefaultsDisabled(nameof(ElasticDefaults.Traces)); + }); + } + else + Logger.LogSignalDisabled(nameof(Signals.Metrics)); - if (options.DistroOptions.EnabledDefaults.HasFlag(ElasticOpenTelemetryOptions.EnabledElasticDefaults.Metrics)) - openTelemetry.WithMetrics(metrics => metrics.UseElasticDefaults(Logger)); + if (distro.Signals.HasFlag(Signals.Metrics)) + { + openTelemetry.WithMetrics(metrics => + { + if (distro.ElasticDefaults.HasFlag(ElasticDefaults.Metrics)) + metrics.UseElasticDefaults(Logger); + else + Logger.LogDefaultsDisabled(nameof(ElasticDefaults.Metrics)); + }); + } + else + Logger.LogSignalDisabled(nameof(Signals.Metrics)); } } @@ -137,4 +168,11 @@ internal static partial class LoggerMessages [LoggerMessage(EventId = 2, Level = LogLevel.Information, Message = "No Elastic defaults were enabled.")] public static partial void LogNoElasticDefaults(this ILogger logger); + + [LoggerMessage(EventId = 1, Level = LogLevel.Information, Message = "ElasticOpenTelemetryBuilder {Signal} skipped, configured to be disabled")] + public static partial void LogSignalDisabled(this ILogger logger, string signal); + + [LoggerMessage(EventId = 1, Level = LogLevel.Information, Message = "Elastic defaults for {Signal} skipped, configured to be disabled")] + public static partial void LogDefaultsDisabled(this ILogger logger, string signal); + } diff --git a/src/Elastic.OpenTelemetry/Extensions/MeterProviderBuilderExtensions.cs b/src/Elastic.OpenTelemetry/Extensions/MeterProviderBuilderExtensions.cs index 968ff5a..88d9282 100644 --- a/src/Elastic.OpenTelemetry/Extensions/MeterProviderBuilderExtensions.cs +++ b/src/Elastic.OpenTelemetry/Extensions/MeterProviderBuilderExtensions.cs @@ -5,6 +5,7 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using OpenTelemetry.Metrics; +using static Elastic.OpenTelemetry.Configuration.Signals; namespace Elastic.OpenTelemetry.Extensions; @@ -23,7 +24,7 @@ public static MeterProviderBuilder UseElasticDefaults(this MeterProviderBuilder .AddRuntimeInstrumentation() .AddHttpClientInstrumentation(); - logger.LogConfiguredSignalProvider("metrics", nameof(MeterProviderBuilder)); + logger.LogConfiguredSignalProvider(nameof(Metrics), nameof(MeterProviderBuilder)); return builder; } diff --git a/src/Elastic.OpenTelemetry/Extensions/OpenTelemetryLoggerOptionsExtensions.cs b/src/Elastic.OpenTelemetry/Extensions/OpenTelemetryLoggerOptionsExtensions.cs index e0b8ac0..72f6638 100644 --- a/src/Elastic.OpenTelemetry/Extensions/OpenTelemetryLoggerOptionsExtensions.cs +++ b/src/Elastic.OpenTelemetry/Extensions/OpenTelemetryLoggerOptionsExtensions.cs @@ -5,6 +5,7 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using OpenTelemetry.Logs; +using static Elastic.OpenTelemetry.Configuration.Signals; namespace Elastic.OpenTelemetry.Extensions; @@ -21,6 +22,6 @@ public static void UseElasticDefaults(this OpenTelemetryLoggerOptions options, I logger ??= NullLogger.Instance; options.IncludeFormattedMessage = true; options.IncludeScopes = true; - logger.LogConfiguredSignalProvider("logging", nameof(OpenTelemetryLoggerOptions)); + logger.LogConfiguredSignalProvider(nameof(Logs), nameof(OpenTelemetryLoggerOptions)); } } diff --git a/src/Elastic.OpenTelemetry/Extensions/TracerProviderBuilderExtensions.cs b/src/Elastic.OpenTelemetry/Extensions/TracerProviderBuilderExtensions.cs index d222dbc..8446490 100644 --- a/src/Elastic.OpenTelemetry/Extensions/TracerProviderBuilderExtensions.cs +++ b/src/Elastic.OpenTelemetry/Extensions/TracerProviderBuilderExtensions.cs @@ -9,6 +9,7 @@ using Microsoft.Extensions.Logging.Abstractions; using OpenTelemetry; using OpenTelemetry.Trace; +using static Elastic.OpenTelemetry.Configuration.Signals; namespace Elastic.OpenTelemetry.Extensions; @@ -44,7 +45,7 @@ public static TracerProviderBuilder UseElasticDefaults(this TracerProviderBuilde .AddEntityFrameworkCoreInstrumentation(); builder.AddElasticProcessors(logger); - logger.LogConfiguredSignalProvider("tracing", nameof(TracerProviderBuilder)); + logger.LogConfiguredSignalProvider(nameof(Traces), nameof(TracerProviderBuilder)); return builder; } } diff --git a/tests/Elastic.OpenTelemetry.Tests/Configuration/ElasticOpenTelemetryOptionsTests.cs b/tests/Elastic.OpenTelemetry.Tests/Configuration/ElasticOpenTelemetryOptionsTests.cs index 6a075ad..9a40c6a 100644 --- a/tests/Elastic.OpenTelemetry.Tests/Configuration/ElasticOpenTelemetryOptionsTests.cs +++ b/tests/Elastic.OpenTelemetry.Tests/Configuration/ElasticOpenTelemetryOptionsTests.cs @@ -3,15 +3,13 @@ // See the LICENSE file in the project root for more information using System.Collections; +using System.Diagnostics.Tracing; 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; @@ -19,14 +17,16 @@ namespace Elastic.OpenTelemetry.Tests.Configuration; public sealed class ElasticOpenTelemetryOptionsTests(ITestOutputHelper output) { + private const int ExpectedLogsLength = 8; + [Fact] public void EnabledElasticDefaults_NoneIncludesExpectedValues() { - var sut = EnabledElasticDefaults.None; + var sut = ElasticDefaults.None; - sut.HasFlag(EnabledElasticDefaults.Tracing).Should().BeFalse(); - sut.HasFlag(EnabledElasticDefaults.Logging).Should().BeFalse(); - sut.HasFlag(EnabledElasticDefaults.Metrics).Should().BeFalse(); + sut.HasFlag(ElasticDefaults.Traces).Should().BeFalse(); + sut.HasFlag(ElasticDefaults.Logs).Should().BeFalse(); + sut.HasFlag(ElasticDefaults.Metrics).Should().BeFalse(); } [Fact] @@ -34,10 +34,10 @@ public void DefaultCtor_SetsExpectedDefaults_WhenNoEnvironmentVariablesAreConfig { var sut = new ElasticOpenTelemetryOptions(new Hashtable { - {OTEL_DOTNET_AUTO_LOG_DIRECTORY, null}, - {OTEL_LOG_LEVEL, null}, - {ELASTIC_OTEL_ENABLE_ELASTIC_DEFAULTS, null}, - {ELASTIC_OTEL_SKIP_OTLP_EXPORTER, null}, + { OTEL_DOTNET_AUTO_LOG_DIRECTORY, null }, + { OTEL_LOG_LEVEL, null }, + { ELASTIC_OTEL_DEFAULTS_ENABLED, null }, + { ELASTIC_OTEL_SKIP_OTLP_EXPORTER, null }, }); sut.GlobalLogEnabled.Should().Be(false); @@ -45,17 +45,17 @@ public void DefaultCtor_SetsExpectedDefaults_WhenNoEnvironmentVariablesAreConfig sut.LogDirectory.Should().Be(sut.LogDirectoryDefault); sut.LogLevel.Should().Be(LogLevel.Warning); - 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.ElasticDefaults.Should().Be(ElasticDefaults.All); + sut.ElasticDefaults.Should().HaveFlag(ElasticDefaults.Traces); + sut.ElasticDefaults.Should().HaveFlag(ElasticDefaults.Metrics); + sut.ElasticDefaults.Should().HaveFlag(ElasticDefaults.Logs); sut.SkipOtlpExporter.Should().Be(false); var logger = new TestLogger(output); sut.LogConfigSources(logger); - logger.Messages.Count.Should().Be(4); + logger.Messages.Count.Should().Be(ExpectedLogsLength); foreach (var message in logger.Messages) message.Should().EndWith("from [Default]"); } @@ -69,25 +69,26 @@ public void DefaultCtor_LoadsConfigurationFromEnvironmentVariables() var sut = new ElasticOpenTelemetryOptions(new Hashtable { - {OTEL_DOTNET_AUTO_LOG_DIRECTORY, fileLogDirectory}, - {OTEL_LOG_LEVEL, fileLogLevel}, - {ELASTIC_OTEL_ENABLE_ELASTIC_DEFAULTS, enabledElasticDefaults}, - {ELASTIC_OTEL_SKIP_OTLP_EXPORTER, "true"}, + { OTEL_DOTNET_AUTO_LOG_DIRECTORY, fileLogDirectory }, + { OTEL_LOG_LEVEL, fileLogLevel }, + { ELASTIC_OTEL_DEFAULTS_ENABLED, 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.ElasticDefaults.Should().Be(ElasticDefaults.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]"); + logger.Messages.Should() + .Contain(s => s.EndsWith("from [Environment]")) + .And.Contain(s => s.EndsWith("from [Default]")) + .And.NotContain(s => s.EndsWith("from [IConfiguration]")); + } @@ -99,23 +100,23 @@ public void ConfigurationCtor_LoadsConfigurationFromIConfiguration() const string enabledElasticDefaults = "None"; var json = $$""" - { - "Logging": { - "LogLevel": { - "Default": "Information", - "Elastic.OpenTelemetry": "{{loggingSectionLogLevel}}" - } - }, - "Elastic": { - "OpenTelemetry": { - "LogDirectory": "C:\\Temp", - "LogLevel": "{{fileLogLevel}}", - "EnabledElasticDefaults": "{{enabledElasticDefaults}}", - "SkipOtlpExporter": true - } - } - } - """; + { + "Logging": { + "LogLevel": { + "Default": "Information", + "Elastic.OpenTelemetry": "{{loggingSectionLogLevel}}" + } + }, + "Elastic": { + "OpenTelemetry": { + "LogDirectory": "C:\\Temp", + "LogLevel": "{{fileLogLevel}}", + "ElasticDefaults": "{{enabledElasticDefaults}}", + "SkipOtlpExporter": true + } + } + } + """; var config = new ConfigurationBuilder() .AddJsonStream(new MemoryStream(Encoding.UTF8.GetBytes(json))) @@ -125,18 +126,20 @@ public void ConfigurationCtor_LoadsConfigurationFromIConfiguration() sut.LogDirectory.Should().Be(@"C:\Temp"); sut.LogLevel.Should().Be(ToLogLevel(fileLogLevel)); - sut.EnableElasticDefaults.Should().Be(enabledElasticDefaults); - sut.EnabledDefaults.Should().Be(EnabledElasticDefaults.None); + sut.ElasticDefaults.Should().Be(ElasticDefaults.None); sut.SkipOtlpExporter.Should().Be(true); - sut.LoggingSectionLogLevel.Should().Be(loggingSectionLogLevel); + sut.EventLogLevel.Should().Be(EventLevel.Warning); + sut.LogLevel.Should().Be(LogLevel.Critical); 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]"); + logger.Messages.Count.Should().Be(ExpectedLogsLength); + logger.Messages.Should() + .Contain(s => s.EndsWith("from [IConfiguration]")) + .And.Contain(s => s.EndsWith("from [Default]")) + .And.NotContain(s => s.EndsWith("from [Environment]")); } [Fact] @@ -146,22 +149,22 @@ public void ConfigurationCtor_LoadsConfigurationFromIConfiguration_AndFallsBackT const string enabledElasticDefaults = "None"; var json = $$""" - { - "Logging": { - "LogLevel": { - "Default": "Information", - "Elastic.OpenTelemetry": "{{loggingSectionLogLevel}}" - } - }, - "Elastic": { - "OpenTelemetry": { - "LogDirectory": "C:\\Temp", - "EnabledElasticDefaults": "{{enabledElasticDefaults}}", - "SkipOtlpExporter": true - } - } - } - """; + { + "Logging": { + "LogLevel": { + "Default": "Information", + "Elastic.OpenTelemetry": "{{loggingSectionLogLevel}}" + } + }, + "Elastic": { + "OpenTelemetry": { + "LogDirectory": "C:\\Temp", + "ElasticDefaults": "{{enabledElasticDefaults}}", + "SkipOtlpExporter": true + } + } + } + """; var config = new ConfigurationBuilder() .AddJsonStream(new MemoryStream(Encoding.UTF8.GetBytes(json))) @@ -171,18 +174,19 @@ public void ConfigurationCtor_LoadsConfigurationFromIConfiguration_AndFallsBackT sut.LogDirectory.Should().Be(@"C:\Temp"); sut.LogLevel.Should().Be(ToLogLevel(loggingSectionLogLevel)); - sut.EnableElasticDefaults.Should().Be(enabledElasticDefaults); - sut.EnabledDefaults.Should().Be(EnabledElasticDefaults.None); + sut.ElasticDefaults.Should().Be(ElasticDefaults.None); sut.SkipOtlpExporter.Should().Be(true); - sut.LoggingSectionLogLevel.Should().Be(loggingSectionLogLevel); + sut.LogLevel.Should().Be(LogLevel.Warning); + sut.EventLogLevel.Should().Be(EventLevel.Warning); 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]"); + logger.Messages.Should() + .Contain(s => s.EndsWith("from [IConfiguration]")) + .And.Contain(s => s.EndsWith("from [Default]")) + .And.NotContain(s => s.EndsWith("from [Environment]")); } @@ -193,21 +197,21 @@ public void ConfigurationCtor_LoadsConfigurationFromIConfiguration_AndFallsBackT const string enabledElasticDefaults = "None"; var json = $$""" - { - "Logging": { - "LogLevel": { - "Default": "{{loggingSectionDefaultLogLevel}}" - } - }, - "Elastic": { - "OpenTelemetry": { - "LogDirectory": "C:\\Temp", - "EnabledElasticDefaults": "{{enabledElasticDefaults}}", - "SkipOtlpExporter": true - } - } - } - """; + { + "Logging": { + "LogLevel": { + "Default": "{{loggingSectionDefaultLogLevel}}" + } + }, + "Elastic": { + "OpenTelemetry": { + "LogDirectory": "C:\\Temp", + "ElasticDefaults": "{{enabledElasticDefaults}}", + "SkipOtlpExporter": true + } + } + } + """; var config = new ConfigurationBuilder() .AddJsonStream(new MemoryStream(Encoding.UTF8.GetBytes(json))) @@ -217,18 +221,18 @@ public void ConfigurationCtor_LoadsConfigurationFromIConfiguration_AndFallsBackT sut.LogDirectory.Should().Be(@"C:\Temp"); sut.LogLevel.Should().Be(ToLogLevel(loggingSectionDefaultLogLevel)); - sut.EnableElasticDefaults.Should().Be(enabledElasticDefaults); - sut.EnabledDefaults.Should().Be(EnabledElasticDefaults.None); + sut.ElasticDefaults.Should().Be(ElasticDefaults.None); sut.SkipOtlpExporter.Should().Be(true); - sut.LoggingSectionLogLevel.Should().Be(loggingSectionDefaultLogLevel); + sut.EventLogLevel.Should().Be(EventLevel.Informational); 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]"); + logger.Messages.Should() + .Contain(s => s.EndsWith("from [IConfiguration]")) + .And.Contain(s => s.EndsWith("from [Default]")) + .And.NotContain(s => s.EndsWith("from [Environment]")); } [Fact] @@ -239,17 +243,17 @@ public void EnvironmentVariables_TakePrecedenceOver_ConfigValues() const string enabledElasticDefaults = "None"; var json = $$""" - { - "Elastic": { - "OpenTelemetry": { - "LogDirectory": "C:\\Json", - "LogLevel": "Trace", - "EnabledElasticDefaults": "All", - "SkipOtlpExporter": false - } - } - } - """; + { + "Elastic": { + "OpenTelemetry": { + "LogDirectory": "C:\\Json", + "LogLevel": "Trace", + "ElasticDefaults": "All", + "SkipOtlpExporter": false + } + } + } + """; var config = new ConfigurationBuilder() .AddJsonStream(new MemoryStream(Encoding.UTF8.GetBytes(json))) @@ -257,16 +261,15 @@ public void EnvironmentVariables_TakePrecedenceOver_ConfigValues() var sut = new ElasticOpenTelemetryOptions(config, new Hashtable { - {OTEL_DOTNET_AUTO_LOG_DIRECTORY, fileLogDirectory}, - {OTEL_LOG_LEVEL, fileLogLevel}, - {ELASTIC_OTEL_ENABLE_ELASTIC_DEFAULTS, enabledElasticDefaults}, - {ELASTIC_OTEL_SKIP_OTLP_EXPORTER, "true"}, + { OTEL_DOTNET_AUTO_LOG_DIRECTORY, fileLogDirectory }, + { OTEL_LOG_LEVEL, fileLogLevel }, + { ELASTIC_OTEL_DEFAULTS_ENABLED, 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.ElasticDefaults.Should().Be(ElasticDefaults.None); sut.SkipOtlpExporter.Should().Be(true); } @@ -275,157 +278,33 @@ public void InitializedProperties_TakePrecedenceOver_EnvironmentValues() { const string fileLogDirectory = "C:\\Property"; const string fileLogLevel = "Critical"; - const string enabledElasticDefaults = "None"; var sut = new ElasticOpenTelemetryOptions(new Hashtable { - {OTEL_DOTNET_AUTO_LOG_DIRECTORY, "C:\\Temp"}, - {OTEL_LOG_LEVEL, "Information"}, - {ELASTIC_OTEL_ENABLE_ELASTIC_DEFAULTS, "All"}, - {ELASTIC_OTEL_SKIP_OTLP_EXPORTER, "true"}, + { OTEL_DOTNET_AUTO_LOG_DIRECTORY, "C:\\Temp" }, + { OTEL_LOG_LEVEL, "Information" }, + { ELASTIC_OTEL_DEFAULTS_ENABLED, "All" }, + { ELASTIC_OTEL_SKIP_OTLP_EXPORTER, "true" }, }) { LogDirectory = fileLogDirectory, LogLevel = ToLogLevel(fileLogLevel) ?? LogLevel.None, SkipOtlpExporter = false, - EnableElasticDefaults = enabledElasticDefaults + ElasticDefaults = ElasticDefaults.None }; sut.LogDirectory.Should().Be(fileLogDirectory); sut.LogLevel.Should().Be(ToLogLevel(fileLogLevel)); - sut.EnableElasticDefaults.Should().Be(enabledElasticDefaults); - sut.EnabledDefaults.Should().Be(EnabledElasticDefaults.None); + sut.ElasticDefaults.Should().Be(ElasticDefaults.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]"); - } - - [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(); + logger.Messages.Should() + .Contain(s => s.EndsWith("from [Property]")) + .And.Contain(s => s.EndsWith("from [Default]")) + .And.NotContain(s => s.EndsWith("from [Environment]")); } } diff --git a/tests/Elastic.OpenTelemetry.Tests/Configuration/EnabledDefaultsConfigurationTests.cs b/tests/Elastic.OpenTelemetry.Tests/Configuration/EnabledDefaultsConfigurationTests.cs new file mode 100644 index 0000000..e022d67 --- /dev/null +++ b/tests/Elastic.OpenTelemetry.Tests/Configuration/EnabledDefaultsConfigurationTests.cs @@ -0,0 +1,124 @@ +// 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 System.Text; +using Elastic.OpenTelemetry.Configuration; +using Microsoft.Extensions.Configuration; +using static Elastic.OpenTelemetry.Configuration.ElasticDefaults; + +namespace Elastic.OpenTelemetry.Tests.Configuration; + +public class ElasticDefaultsConfigurationTest +{ + + [Theory] + [ClassData(typeof(DefaultsData))] + public void ParsesFromConfiguration(string optionValue, Action asserts) + { + var json = $$""" + { + "Elastic": { + "OpenTelemetry": { + "ElasticDefaults": "{{optionValue}}", + } + } + } + """; + + var config = new ConfigurationBuilder() + .AddJsonStream(new MemoryStream(Encoding.UTF8.GetBytes(json))) + .Build(); + var sut = new ElasticOpenTelemetryOptions(config, new Hashtable()); + asserts(sut.ElasticDefaults); + } + + [Theory] + [ClassData(typeof(DefaultsData))] + internal void ParseFromEnvironment(string optionValue, Action asserts) + { + + var env = new Hashtable { { EnvironmentVariables.ELASTIC_OTEL_DEFAULTS_ENABLED, optionValue } }; + var sut = new ElasticOpenTelemetryOptions(env); + + asserts(sut.ElasticDefaults); + } + + internal class DefaultsData : TheoryData> + { + public DefaultsData() + { + Add("All", a => + { + a.HasFlag(Traces).Should().BeTrue(); + a.HasFlag(Metrics).Should().BeTrue(); + a.HasFlag(Logs).Should().BeTrue(); + a.Equals(None).Should().BeFalse(); + }); + + Add("all", a => + { + a.HasFlag(Traces).Should().BeTrue(); + a.HasFlag(Metrics).Should().BeTrue(); + a.HasFlag(Logs).Should().BeTrue(); + a.Equals(None).Should().BeFalse(); + }); + + Add("Traces", a => + { + a.HasFlag(Traces).Should().BeTrue(); + a.HasFlag(Metrics).Should().BeFalse(); + a.HasFlag(Logs).Should().BeFalse(); + a.Equals(None).Should().BeFalse(); + }); + + Add("Metrics", a => + { + a.HasFlag(Traces).Should().BeFalse(); + a.HasFlag(Metrics).Should().BeTrue(); + a.HasFlag(Logs).Should().BeFalse(); + a.Equals(None).Should().BeFalse(); + }); + + Add("Logs", a => + { + a.HasFlag(Traces).Should().BeFalse(); + a.HasFlag(Metrics).Should().BeFalse(); + a.HasFlag(Logs).Should().BeTrue(); + a.Equals(None).Should().BeFalse(); + }); + + Add("Traces,Logs", a => + { + a.HasFlag(Traces).Should().BeTrue(); + a.HasFlag(Metrics).Should().BeFalse(); + a.HasFlag(Logs).Should().BeTrue(); + a.Equals(None).Should().BeFalse(); + }); + Add("Traces;Logs", a => + { + a.HasFlag(Traces).Should().BeTrue(); + a.HasFlag(Metrics).Should().BeFalse(); + a.HasFlag(Logs).Should().BeTrue(); + a.Equals(None).Should().BeFalse(); + }); + + Add("traces,logs,metrics", a => + { + a.HasFlag(Traces).Should().BeTrue(); + a.HasFlag(Metrics).Should().BeTrue(); + a.HasFlag(Logs).Should().BeTrue(); + a.Equals(None).Should().BeFalse(); + }); + + Add("None", a => + { + a.HasFlag(Traces).Should().BeFalse(); + a.HasFlag(Metrics).Should().BeFalse(); + a.HasFlag(Logs).Should().BeFalse(); + a.Equals(None).Should().BeTrue(); + }); + } + }; +} diff --git a/tests/Elastic.OpenTelemetry.Tests/Configuration/EnabledSignalsConfigurationTests.cs b/tests/Elastic.OpenTelemetry.Tests/Configuration/EnabledSignalsConfigurationTests.cs new file mode 100644 index 0000000..3f660d8 --- /dev/null +++ b/tests/Elastic.OpenTelemetry.Tests/Configuration/EnabledSignalsConfigurationTests.cs @@ -0,0 +1,248 @@ +// 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 System.Text; +using Elastic.OpenTelemetry.Configuration; +using Elastic.OpenTelemetry.Configuration.Instrumentations; +using Elastic.OpenTelemetry.Extensions; +using Microsoft.Extensions.Configuration; +using OpenTelemetry; +using Xunit.Abstractions; +using static Elastic.OpenTelemetry.Configuration.EnvironmentVariables; +using static Elastic.OpenTelemetry.Configuration.Signals; + +namespace Elastic.OpenTelemetry.Tests.Configuration; + +public class EnabledSignalsConfigurationTest(ITestOutputHelper output) +{ + + [Theory] + [ClassData(typeof(SignalsAsStringInConfigurationData))] + public void ParsesFromConfiguration(string optionValue, Action asserts) + { + var json = $$""" + { + "Elastic": { + "OpenTelemetry": { + "Signals": "{{optionValue}}", + } + } + } + """; + + var config = new ConfigurationBuilder() + .AddJsonStream(new MemoryStream(Encoding.UTF8.GetBytes(json))) + .Build(); + var sut = new ElasticOpenTelemetryOptions(config, new Hashtable()); + asserts(sut.Signals); + } + + [Fact] + internal void ExplicitlySettingASignalDoesNotDisableOthers() + { + var env = new Hashtable { { OTEL_DOTNET_AUTO_LOGS_INSTRUMENTATION_ENABLED, "1" } }; + var options = new ElasticOpenTelemetryOptions(env); + options.Signals.Should().HaveFlag(Logs); + options.Signals.Should().HaveFlag(Metrics); + options.Signals.Should().HaveFlag(Traces); + options.Signals.Should().HaveFlag(All); + } + + [Fact] + internal void ExplicitlyDisablingASignalDoesNotDisableOthers() + { + var env = new Hashtable { { OTEL_DOTNET_AUTO_LOGS_INSTRUMENTATION_ENABLED, "0" } }; + var options = new ElasticOpenTelemetryOptions(env); + options.Signals.Should().NotHaveFlag(Logs); + options.Signals.Should().HaveFlag(Metrics); + options.Signals.Should().HaveFlag(Traces); + options.Signals.Should().NotHaveFlag(All); + } + + [Fact] + public void OptInFromConfig() + { + var json = $$""" + { + "Elastic": { + "OpenTelemetry": { + "Signals": "All", + "Tracing" : "AspNet;ElasticTransport" + } + } + } + """; + + var config = new ConfigurationBuilder() + .AddJsonStream(new MemoryStream(Encoding.UTF8.GetBytes(json))) + .Build(); + var options = new ElasticOpenTelemetryOptions(config, new Hashtable()); + + options.Tracing.Should().HaveCount(2); + } + [Fact] + public void OptOutFromConfig() + { + var json = $$""" + { + "Elastic": { + "OpenTelemetry": { + "Signals": "All", + "Tracing" : "-AspNet;-ElasticTransport" + } + } + } + """; + + var config = new ConfigurationBuilder() + .AddJsonStream(new MemoryStream(Encoding.UTF8.GetBytes(json))) + .Build(); + + var logger = new TestLogger(output); + var options = new ElasticOpenTelemetryOptions(config, new Hashtable()); + options.LogConfigSources(logger); + + options.Tracing.Should().HaveCount(TraceInstrumentations.All.Count - 2); + + logger.Messages.Should().ContainMatch("*Configured value for Tracing: 'All Except: AspNet, ElasticTransport'*"); + } + + + [Theory] + [InlineData("1", "1", true, true)] + [InlineData("0", "1", true, false)] + [InlineData("0", "0", false, false)] + [InlineData("1", "0", false, true)] + internal void RespectsOveralSignalsEnvironmentVar(string instrumentation, string metrics, bool metricsEnabled, bool traceEnabled) + { + var env = new Hashtable { { OTEL_DOTNET_AUTO_INSTRUMENTATION_ENABLED, instrumentation }, { OTEL_DOTNET_AUTO_METRICS_INSTRUMENTATION_ENABLED, metrics } }; + var options = new ElasticOpenTelemetryOptions(env); + if (metricsEnabled) + options.Signals.Should().HaveFlag(Metrics); + else + options.Signals.Should().NotHaveFlag(Metrics); + + if (traceEnabled) + options.Signals.Should().HaveFlag(Traces); + else + options.Signals.Should().NotHaveFlag(Traces); + + if (instrumentation == "0" && metrics == "0") + options.Signals.Should().Be(None); + else + options.Signals.Should().NotBe(None); + } + + [Theory] + [InlineData("1", "0", true)] + [InlineData("0", "1", true)] + [InlineData("0", "0", false)] + internal void OptInOverridesDefaults(string instrumentation, string metrics, bool enabledMetrics) + { + var env = new Hashtable + { + { OTEL_DOTNET_AUTO_INSTRUMENTATION_ENABLED, instrumentation }, + { "OTEL_DOTNET_AUTO_METRICS_ASPNET_INSTRUMENTATION_ENABLED", metrics } + }; + var options = new ElasticOpenTelemetryOptions(env); + if (metrics == "1") + { + options.Metrics.Should().Contain(MetricInstrumentation.AspNet); + //ensure opt in behavior + if (instrumentation == "0") + options.Metrics.Should().HaveCount(1); + //ensure opt out behaviour + else + options.Metrics.Should().HaveCount(MetricInstrumentations.All.Count - 1); + + } + else + options.Metrics.Should().NotContain(MetricInstrumentation.AspNet); + + if (enabledMetrics) + options.Signals.Should().HaveFlag(Metrics); + else + options.Signals.Should().NotHaveFlag(Metrics); + } + + + + private class SignalsAsStringInConfigurationData : TheoryData> + { + public SignalsAsStringInConfigurationData() + { + Add("All", a => + { + a.HasFlag(Traces).Should().BeTrue(); + a.HasFlag(Metrics).Should().BeTrue(); + a.HasFlag(Logs).Should().BeTrue(); + a.Equals(None).Should().BeFalse(); + }); + + Add("all", a => + { + a.HasFlag(Traces).Should().BeTrue(); + a.HasFlag(Metrics).Should().BeTrue(); + a.HasFlag(Logs).Should().BeTrue(); + a.Equals(None).Should().BeFalse(); + }); + + Add("Traces", a => + { + a.HasFlag(Traces).Should().BeTrue(); + a.HasFlag(Metrics).Should().BeFalse(); + a.HasFlag(Logs).Should().BeFalse(); + a.Equals(None).Should().BeFalse(); + }); + + Add("Metrics", a => + { + a.HasFlag(Traces).Should().BeFalse(); + a.HasFlag(Metrics).Should().BeTrue(); + a.HasFlag(Logs).Should().BeFalse(); + a.Equals(None).Should().BeFalse(); + }); + + Add("Logs", a => + { + a.HasFlag(Traces).Should().BeFalse(); + a.HasFlag(Metrics).Should().BeFalse(); + a.HasFlag(Logs).Should().BeTrue(); + a.Equals(None).Should().BeFalse(); + }); + + Add("Traces,Logs", a => + { + a.HasFlag(Traces).Should().BeTrue(); + a.HasFlag(Metrics).Should().BeFalse(); + a.HasFlag(Logs).Should().BeTrue(); + a.Equals(None).Should().BeFalse(); + }); + Add("Traces;Logs", a => + { + a.HasFlag(Traces).Should().BeTrue(); + a.HasFlag(Metrics).Should().BeFalse(); + a.HasFlag(Logs).Should().BeTrue(); + a.Equals(None).Should().BeFalse(); + }); + + Add("traces,logs,metrics", a => + { + a.HasFlag(Traces).Should().BeTrue(); + a.HasFlag(Metrics).Should().BeTrue(); + a.HasFlag(Logs).Should().BeTrue(); + a.Equals(None).Should().BeFalse(); + }); + + Add("None", a => + { + a.HasFlag(Traces).Should().BeFalse(); + a.HasFlag(Metrics).Should().BeFalse(); + a.HasFlag(Logs).Should().BeFalse(); + a.Equals(None).Should().BeTrue(); + }); + } + }; +} diff --git a/tests/Elastic.OpenTelemetry.Tests/Processors/TransactionProcessorTests.cs b/tests/Elastic.OpenTelemetry.Tests/Processors/TransactionProcessorTests.cs new file mode 100644 index 0000000..5ea3cbc --- /dev/null +++ b/tests/Elastic.OpenTelemetry.Tests/Processors/TransactionProcessorTests.cs @@ -0,0 +1,52 @@ +// 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 Elastic.OpenTelemetry.Configuration; +using OpenTelemetry; +using Xunit.Abstractions; +using OpenTelemetryBuilderExtensions = Elastic.OpenTelemetry.Extensions.OpenTelemetryBuilderExtensions; + +namespace Elastic.OpenTelemetry.Tests.Processors; + +public class TransactionProcessorTests(ITestOutputHelper output) +{ + [Fact] + public void TransactionId_IsNotAdded_WhenElasticDefaultsDoesNotIncludeTracing() + { + var options = new ElasticOpenTelemetryBuilderOptions + { + Logger = new TestLogger(output), + DistroOptions = new ElasticOpenTelemetryOptions() + { + SkipOtlpExporter = true, + ElasticDefaults = ElasticDefaults.None + } + }; + + const string activitySourceName = nameof(TransactionId_IsNotAdded_WhenElasticDefaultsDoesNotIncludeTracing); + + var activitySource = new ActivitySource(activitySourceName, "1.0.0"); + + var exportedItems = new List(); + + using var session = OpenTelemetryBuilderExtensions.Build(new ElasticOpenTelemetryBuilder(options) + .WithTracing(tpb => + { + tpb + .ConfigureResource(rb => rb.AddService("Test", "1.0.0")) + .AddSource(activitySourceName).AddInMemoryExporter(exportedItems); + })); + + 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(); + } +}