src/Elastic.OpenTelemetry.Core/Configuration/CompositeElasticOpenTelemetryOptions.cs (236 lines of code) (raw):
// 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.Diagnostics.CodeAnalysis;
using System.Diagnostics.Tracing;
using System.Runtime.InteropServices;
using Elastic.OpenTelemetry.Configuration.Instrumentations;
using Elastic.OpenTelemetry.Configuration.Parsers;
using Elastic.OpenTelemetry.Core;
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;
/// <summary>
/// Defines advanced options which can be used to finely-tune the behaviour of the Elastic
/// distribution of OpenTelemetry.
/// </summary>
/// <remarks>
/// Options are bound from the following sources:
/// <list type="bullet">
/// <item><description>Environment variables</description></item>
/// <item><description>An <see cref="IConfiguration"/> instance</description></item>
/// </list>
/// Options initialised via property initializers take precedence over bound values.
/// Environment variables take precedence over <see cref="IConfiguration"/> values.
/// </remarks>
internal sealed class CompositeElasticOpenTelemetryOptions
{
private readonly EventLevel _eventLevel = EventLevel.Informational;
private readonly ConfigCell<string?> _logDirectory = new(nameof(LogDirectory), null);
private readonly ConfigCell<LogTargets?> _logTargets = new(nameof(LogTargets), null);
private readonly ConfigCell<LogLevel?> _logLevel = new(nameof(LogLevel), LogLevel.Warning);
private readonly ConfigCell<bool?> _skipOtlpExporter = new(nameof(SkipOtlpExporter), false);
private readonly ConfigCell<bool?> _skipInstrumentationAssemblyScanning = new(nameof(SkipInstrumentationAssemblyScanning), false);
private readonly ConfigCell<bool?> _runningInContainer = new(nameof(_runningInContainer), false);
private readonly ConfigCell<Signals?> _signals = new(nameof(Signals), Signals.All);
private readonly ConfigCell<TraceInstrumentations> _tracing = new(nameof(Tracing), TraceInstrumentations.All);
private readonly ConfigCell<MetricInstrumentations> _metrics = new(nameof(Metrics), MetricInstrumentations.All);
private readonly ConfigCell<LogInstrumentations> _logging = new(nameof(Logging), LogInstrumentations.All);
private readonly IDictionary _environmentVariables;
internal static CompositeElasticOpenTelemetryOptions DefaultOptions = new();
internal static CompositeElasticOpenTelemetryOptions SkipOtlpOptions = new() { SkipOtlpExporter = true };
/// <summary>
/// Creates a new instance of <see cref="CompositeElasticOpenTelemetryOptions"/> with properties
/// bound from environment variables.
/// </summary>
internal CompositeElasticOpenTelemetryOptions() : this((IDictionary?)null)
{
}
internal CompositeElasticOpenTelemetryOptions(IDictionary? environmentVariables)
{
LogDirectoryDefault = GetDefaultLogDirectory();
_environmentVariables = environmentVariables ?? GetEnvironmentVariables();
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_SKIP_ASSEMBLY_SCANNING, _skipInstrumentationAssemblyScanning, BoolParser);
var parser = new EnvironmentParser(_environmentVariables);
parser.ParseInstrumentationVariables(_signals, _tracing, _metrics, _logging);
}
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "Manually verified")]
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresDynamicCode", Justification = "Manually verified")]
internal CompositeElasticOpenTelemetryOptions(IConfiguration? configuration, IDictionary? environmentVariables = null)
: this(environmentVariables)
{
if (configuration is null)
return;
var parser = new ConfigurationParser(configuration);
parser.ParseLogDirectory(_logDirectory);
parser.ParseLogTargets(_logTargets);
parser.ParseLogLevel(_logLevel, ref _eventLevel);
parser.ParseSkipOtlpExporter(_skipOtlpExporter);
parser.ParseSkipInstrumentationAssemblyScanning(_skipInstrumentationAssemblyScanning);
}
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "Manually verified")]
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresDynamicCode", Justification = "Manually verified")]
internal CompositeElasticOpenTelemetryOptions(IConfiguration configuration, ElasticOpenTelemetryOptions options)
: this((IDictionary?)null)
{
var parser = new ConfigurationParser(configuration);
parser.ParseLogDirectory(_logDirectory);
parser.ParseLogTargets(_logTargets);
parser.ParseLogLevel(_logLevel, ref _eventLevel);
parser.ParseSkipOtlpExporter(_skipOtlpExporter);
parser.ParseSkipInstrumentationAssemblyScanning(_skipInstrumentationAssemblyScanning);
if (options.SkipOtlpExporter.HasValue)
_skipOtlpExporter.Assign(options.SkipOtlpExporter.Value, ConfigSource.Options);
if (!string.IsNullOrEmpty(options.LogDirectory))
_logDirectory.Assign(options.LogDirectory, ConfigSource.Options);
if (options.LogLevel.HasValue)
_logLevel.Assign(options.LogLevel.Value, ConfigSource.Options);
if (options.LogTargets.HasValue)
_logTargets.Assign(options.LogTargets.Value, ConfigSource.Options);
if (options.SkipInstrumentationAssemblyScanning.HasValue)
_skipInstrumentationAssemblyScanning.Assign(options.SkipInstrumentationAssemblyScanning.Value, ConfigSource.Options);
AdditionalLogger = options.AdditionalLogger ?? options.AdditionalLoggerFactory?.CreateElasticLogger();
}
internal CompositeElasticOpenTelemetryOptions(ElasticOpenTelemetryOptions options)
: this((IDictionary?)null)
{
if (options is null)
return;
// Having configured the base settings from env vars, we now override anything that was
// explicitly configured in the user provided options.
if (options.SkipOtlpExporter.HasValue)
_skipOtlpExporter.Assign(options.SkipOtlpExporter.Value, ConfigSource.Options);
if (!string.IsNullOrEmpty(options.LogDirectory))
_logDirectory.Assign(options.LogDirectory, ConfigSource.Options);
if (options.LogLevel.HasValue)
_logLevel.Assign(options.LogLevel.Value, ConfigSource.Options);
if (options.LogTargets.HasValue)
_logTargets.Assign(options.LogTargets.Value, ConfigSource.Options);
if (options.SkipInstrumentationAssemblyScanning.HasValue)
_skipInstrumentationAssemblyScanning.Assign(options.SkipInstrumentationAssemblyScanning.Value, ConfigSource.Options);
AdditionalLogger = options.AdditionalLogger ?? options.AdditionalLoggerFactory?.CreateElasticLogger();
}
internal CompositeElasticOpenTelemetryOptions(IConfiguration configuration, ILoggerFactory loggerFactory) :
this(configuration) => AdditionalLogger = loggerFactory?.CreateElasticLogger();
/// <summary>
/// Calculates whether global logging is enabled based on
/// <see cref="LogTargets"/>, <see cref="LogDirectory"/> and <see cref="LogLevel"/>
/// </summary>
internal bool GlobalLogEnabled
{
get
{
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 (level is LogLevel.None)
isActive = false;
else if (targets is LogTargets.None)
isActive = false;
return isActive;
}
}
private static string GetDefaultLogDirectory()
{
const string applicationMoniker = "elastic-otel-dotnet";
if (IsOSPlatform(OSPlatform.Windows))
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}";
}
/// <summary>
/// The default log directory if file logging was enabled but non was specified
/// <para>Defaults to: </para>
/// <para> - %USERPROFILE%\AppData\Roaming\elastic\elastic-otel-dotnet (on Windows)</para>
/// <para> - /var/log/elastic/elastic-otel-dotnet (on Linux)</para>
/// <para> - ~/Library/Application Support/elastic/elastic-otel-dotnet (on OSX)</para>
/// </summary>
internal string LogDirectoryDefault { get; }
/// <inheritdoc cref="ElasticOpenTelemetryOptions.LogDirectory"/>
public string LogDirectory
{
get => _logDirectory.Value ?? LogDirectoryDefault;
init => _logDirectory.Assign(value, ConfigSource.Property);
}
/// <summary>
/// Used by <see cref="LoggingEventListener"/> to determine the appropiate event level to subscribe to
/// </summary>
internal EventLevel EventLogLevel => _eventLevel;
/// <inheritdoc cref="ElasticOpenTelemetryOptions.LogLevel"/>
public LogLevel LogLevel
{
get => _logLevel.Value ?? LogLevel.Warning;
init => _logLevel.Assign(value, ConfigSource.Property);
}
/// <inheritdoc cref="ElasticOpenTelemetryOptions.LogTargets"/>
public LogTargets LogTargets
{
get => _logTargets.Value ?? (GlobalLogEnabled
? _runningInContainer.Value.HasValue && _runningInContainer.Value.Value ? LogTargets.StdOut : LogTargets.File
: LogTargets.None);
init => _logTargets.Assign(value, ConfigSource.Property);
}
/// <inheritdoc cref="ElasticOpenTelemetryOptions.SkipOtlpExporter"/>
public bool SkipOtlpExporter
{
get => _skipOtlpExporter.Value ?? false;
init => _skipOtlpExporter.Assign(value, ConfigSource.Property);
}
/// <inheritdoc cref="ElasticOpenTelemetryOptions.SkipInstrumentationAssemblyScanning"/>
public bool SkipInstrumentationAssemblyScanning
{
get => _skipInstrumentationAssemblyScanning.Value ?? false;
init => _skipInstrumentationAssemblyScanning.Assign(value, ConfigSource.Property);
}
public ILogger? AdditionalLogger { get; internal set; }
/// <summary>
/// Control which signals will be automatically enabled by the Elastic Distribution of OpenTelemetry .NET.
/// <para>
/// This configuration respects the open telemetry environment configuration out of the box:
/// <list type="bullet">
/// <item><see cref="OTEL_DOTNET_AUTO_TRACES_INSTRUMENTATION_ENABLED"/></item>
/// <item><see cref="OTEL_DOTNET_AUTO_LOGS_INSTRUMENTATION_ENABLED"/></item>
/// <item><see cref="OTEL_DOTNET_AUTO_METRICS_INSTRUMENTATION_ENABLED"/></item>
/// </list>
/// </para>
/// <para>Setting this propery in code or configuration will take precedence over environment variables.</para>
/// </summary>
public Signals Signals
{
get => _signals.Value ?? Signals.All;
init => _signals.Assign(value, ConfigSource.Property);
}
/// <summary>
/// Enabled trace instrumentations.
/// </summary>
public TraceInstrumentations Tracing
{
get => _tracing.Value ?? TraceInstrumentations.All;
init => _tracing.Assign(value, ConfigSource.Property);
}
/// <summary>
/// Enabled trace instrumentations.
/// </summary>
public MetricInstrumentations Metrics
{
get => _metrics.Value ?? MetricInstrumentations.All;
init => _metrics.Assign(value, ConfigSource.Property);
}
/// <summary>
/// Enabled trace instrumentations.
/// </summary>
public LogInstrumentations Logging
{
get => _logging.Value ?? LogInstrumentations.All;
init => _logging.Assign(value, ConfigSource.Property);
}
public override bool Equals(object? obj)
{
if (obj is not CompositeElasticOpenTelemetryOptions other)
return false;
return LogDirectory == other.LogDirectory &&
LogLevel == other.LogLevel &&
LogTargets == other.LogTargets &&
SkipOtlpExporter == other.SkipOtlpExporter &&
Signals == other.Signals &&
Tracing.SetEquals(other.Tracing) &&
Metrics.SetEquals(other.Metrics) &&
Logging.SetEquals(other.Logging) &&
ReferenceEquals(AdditionalLogger, other.AdditionalLogger);
}
public override int GetHashCode()
{
#if NET462 || NETSTANDARD2_0
return LogDirectory.GetHashCode()
^ LogLevel.GetHashCode()
^ LogTargets.GetHashCode()
^ SkipOtlpExporter.GetHashCode()
^ Signals.GetHashCode()
^ Tracing.GetHashCode()
^ Metrics.GetHashCode()
^ Logging.GetHashCode()
^ (AdditionalLogger?.GetHashCode() ?? 0);
#else
var hash1 = HashCode.Combine(LogDirectory, LogLevel, LogTargets, SkipOtlpExporter);
var hash2 = HashCode.Combine(Signals, Tracing, Metrics, Logging, AdditionalLogger);
return HashCode.Combine(hash1, hash2);
#endif
}
private void SetFromEnvironment<T>(string key, ConfigCell<T> field, Func<string?, T?> parser)
{
var safeValue = GetSafeEnvironmentVariable(key);
// 'Trace' does not exist in OTEL_LOG_LEVEL ensure we parse it to next granularity
// debug, NOTE that OpenTelemetry treats this as invalid and will parse to 'Information'
// We treat 'Debug' and 'Trace' as a signal global file logging should be enabled.
if (key.Equals("OTEL_LOG_LEVEL") && safeValue.Equals("trace", StringComparison.OrdinalIgnoreCase))
safeValue = "debug";
var value = parser(safeValue);
if (value is null)
return;
field.Assign(value, ConfigSource.Environment);
}
private string GetSafeEnvironmentVariable(string key)
{
var value = _environmentVariables.Contains(key) ? _environmentVariables[key]?.ToString() : null;
return value ?? string.Empty;
}
internal void LogConfigSources(ILogger logger)
{
logger.LogDebug("Configured value for {Configuration}", _logDirectory);
logger.LogDebug("Configured value for {Configuration}", _logLevel);
logger.LogDebug("Configured value for {Configuration}", _skipOtlpExporter);
logger.LogDebug("Configured value for {Configuration}", _signals);
logger.LogDebug("Configured value for {Configuration}", _tracing);
logger.LogDebug("Configured value for {Configuration}", _metrics);
logger.LogDebug("Configured value for {Configuration}", _logging);
}
}