src/Elastic.OpenTelemetry.Core/SignalBuilder.cs (169 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.Diagnostics.CodeAnalysis;
using System.Reflection;
using System.Runtime.CompilerServices;
using Elastic.OpenTelemetry.Configuration;
using Elastic.OpenTelemetry.Diagnostics;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
namespace Elastic.OpenTelemetry.Core;
/// <summary>
/// Provides static helper methods which centralise provider builder logic used when registering EDOT
/// defaults on the various builders.
/// </summary>
internal static class SignalBuilder
{
#pragma warning disable IDE0028 // Simplify collection initialization
private static readonly ConditionalWeakTable<object, BuilderState> BuilderStateTable = new();
#pragma warning restore IDE0028 // Simplify collection initialization
private static readonly Lock Lock = new();
/// <summary>
/// Returns the most relevant <see cref="ILogger"/> for builder extension methods to use.
/// </summary>
public static ILogger GetLogger(
ElasticOpenTelemetryComponents? components,
CompositeElasticOpenTelemetryOptions? options) =>
components?.Logger ?? options?.AdditionalLogger ?? NullLogger.Instance;
/// <summary>
/// Returns the most relevant <see cref="ILogger"/> for builder extension methods to use.
/// </summary>
public static ILogger GetLogger<T>(
T builder,
ElasticOpenTelemetryComponents? components,
CompositeElasticOpenTelemetryOptions? options,
BuilderState? builderState) where T : class
{
if (builderState is not null)
return builderState.Components.Logger;
var logger = components?.Logger ?? options?.AdditionalLogger ?? NullLogger.Instance;
if (BuilderStateTable.TryGetValue(builder, out builderState))
logger = builderState.Components.Logger;
return logger;
}
public static T WithElasticDefaults<T>(
T builder,
Signals signal,
CompositeElasticOpenTelemetryOptions? options,
ElasticOpenTelemetryComponents? components,
IServiceCollection? services,
Action<T, BuilderState, IServiceCollection?> configure) where T : class
{
var providerBuilderName = builder.GetType().Name;
var logger = GetLogger(components, options);
BuilderState? builderState = null;
try
{
var builderInstanceId = "<unknown>";
if (BuilderStateTable.TryGetValue(builder, out var existingBuilderState))
{
builderState = existingBuilderState;
builderInstanceId = existingBuilderState.InstanceIdentifier;
}
if (builderState is not null)
logger = builderState.Components.Logger;
// If the signal is disabled via configuration we skip any potential bootstrapping.
var configuredSignals = components?.Options.Signals ?? options?.Signals ?? Signals.All;
if (!configuredSignals.HasFlagFast(signal))
{
logger.LogSignalDisabled(signal.ToString().ToLower(), providerBuilderName, builderInstanceId);
return builder;
}
return WithElasticDefaults(builder, options, components, services, configure);
}
catch (Exception ex)
{
var signalNameForLogging = signal.ToStringFast().ToLowerInvariant();
logger.LogError(new EventId(501, "BuilderDefaultsFailed"), ex, "Failed to fully register EDOT .NET " +
"{Signal} defaults on the {ProviderBuilderName}.", signalNameForLogging, providerBuilderName);
}
return builder;
}
public static T WithElasticDefaults<T>(
T builder,
CompositeElasticOpenTelemetryOptions? options,
ElasticOpenTelemetryComponents? components,
IServiceCollection? services,
Action<T, BuilderState, IServiceCollection?> configure) where T : class
{
var providerBuilderName = builder.GetType().Name;
var logger = GetLogger(components, options);
try
{
if (BuilderStateTable.TryGetValue(builder, out var builderState))
{
builderState.Components.Logger.LogBuilderAlreadyConfigured(providerBuilderName, builderState.InstanceIdentifier);
// This allows us to track the number of times a specific instance of a builder is configured.
// We expect each builder to be configured at most once and log a warning if multiple invocations
// are detected.
builderState.IncrementWithElasticDefaults();
if (builderState.WithElasticDefaultsCounter > 1)
builderState.Components.Logger.LogWarning("The `WithElasticDefaults` method has been called {WithElasticDefaultsCount} " +
"times on the same `{BuilderType}` (instance: {BuilderInstanceId}). This method is " +
"expected to be invoked a maximum of one time.", builderState.WithElasticDefaultsCounter, providerBuilderName,
builderState.InstanceIdentifier);
return builder;
}
// This should not be a hot path, so locking here is reasonable.
using (var scope = Lock.EnterScope())
{
var instanceId = Guid.NewGuid().ToString(); // Used in logging to track duplicate calls to the same builder
// We can't log to the file here as we don't yet have any bootstrapped components.
// Therefore, this message will only appear if the consumer provides an additional logger.
// This is fine as it's a trace level message for advanced debugging.
logger.LogNoExistingComponents(providerBuilderName, instanceId);
options ??= CompositeElasticOpenTelemetryOptions.DefaultOptions;
components = ElasticOpenTelemetry.Bootstrap(options, services);
builderState = new BuilderState(components, instanceId);
configure(builder, builderState, services);
components.Logger.LogStoringBuilderState(providerBuilderName, instanceId);
BuilderStateTable.Add(builder, builderState);
}
}
catch (Exception ex)
{
logger.LogError(new EventId(502, "BuilderDefaultsFailed"), ex, "Failed to fully register EDOT .NET " +
"defaults on the {ProviderBuilderName}.", builder.GetType().Name);
}
return builder;
}
/// <summary>
/// Identifies whether a specific instrumentation assembly is present alongside the executing application.
/// </summary>
public static bool InstrumentationAssemblyExists(string assemblyName)
{
var assemblyLocation = Path.GetDirectoryName(AppContext.BaseDirectory);
if (string.IsNullOrEmpty(assemblyLocation))
return false;
return File.Exists(Path.Combine(assemblyLocation, assemblyName));
}
[RequiresUnreferencedCode("Accesses assemblies and methods dynamically using refelction. This is by design and cannot be made trim compatible.")]
public static void AddInstrumentationViaReflection<T>(T builder, ElasticOpenTelemetryComponents components, ReadOnlySpan<InstrumentationAssemblyInfo> assemblyInfos, string builderInstanceId)
where T : class
{
if (components.Options.SkipInstrumentationAssemblyScanning)
{
components.Logger.LogSkippingAssemblyScanning(builder.GetType().Name, builderInstanceId);
return;
}
AddInstrumentationViaReflection(builder, components.Logger, assemblyInfos, builderInstanceId);
}
[RequiresUnreferencedCode("Accesses assemblies and methods dynamically using refelction. This is by design and cannot be made trim compatible.")]
public static void AddInstrumentationViaReflection<T>(T builder, ILogger logger, ReadOnlySpan<InstrumentationAssemblyInfo> assemblyInfos, string builderInstanceId)
where T : class
{
var builderTypeName = builder.GetType().Name;
foreach (var assemblyInfo in assemblyInfos)
{
try
{
var type = Type.GetType($"{assemblyInfo.FullyQualifiedType}, {assemblyInfo.AssemblyName}");
if (type is null)
{
logger.LogUnableToFindType(assemblyInfo.FullyQualifiedType, assemblyInfo.AssemblyName);
continue;
}
var methodInfo = type.GetMethod(assemblyInfo.InstrumentationMethod, BindingFlags.Static | BindingFlags.Public,
Type.DefaultBinder, [typeof(T)], null);
if (methodInfo is null)
{
logger.LogUnableToFindMethodWarning(assemblyInfo.FullyQualifiedType, assemblyInfo.InstrumentationMethod,
assemblyInfo.AssemblyName);
continue;
}
methodInfo.Invoke(null, [builder]); // Invoke the extension method to register the instrumentation with the builder.
if (builderTypeName.StartsWith("ResourceBuilder"))
{
logger.LogAddedResourceDetectorViaReflection(assemblyInfo.Name, builderTypeName, builderInstanceId);
}
else
{
logger.LogAddedInstrumentationViaReflection(assemblyInfo.Name, builderTypeName, builderInstanceId);
}
}
catch (Exception ex)
{
logger.LogError(new EventId(503, "DynamicInstrumentaionFailed"), ex, "Failed to dynamically enable " +
"{InstrumentationName} on {Provider}.", assemblyInfo.Name, builderTypeName);
}
}
}
}