Skip to content

Commit

Permalink
Rework logging so everything is ILogger based (#45)
Browse files Browse the repository at this point in the history
* Make FileLogger an ILogger implementation

* fix boolean logic isenabled

* add simple logging assertions

* Update example to include activity source name

* Update logging test so that agent is disposed before we validate logs

* Return new list

* ensure AgentBuilder.Agent disposes all

* remove ElasticDiagnosticLoggingObserver so logging is no longer duplicated over all active loggers

* Remove Agent.Current, rely only on AgentBuilder" (#46)
  • Loading branch information
Mpdreamz authored Mar 4, 2024
1 parent 608332c commit 37ad73e
Show file tree
Hide file tree
Showing 30 changed files with 682 additions and 970 deletions.
2 changes: 1 addition & 1 deletion examples/Example.Elastic.OpenTelemetry.Worker/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

var builder = Host.CreateApplicationBuilder(args);

builder.AddElasticOpenTelemetry("CustomActivitySource", "CustomMeter");
builder.Services.AddElasticOpenTelemetry(Worker.ActivitySourceName, "CustomMeter");

builder.Services.AddHostedService<Worker>();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"dotnetRunMessages": true,
"environmentVariables": {
"DOTNET_ENVIRONMENT": "Development",
"OTEL_RESOURCE_ATTRIBUTES": "service.name=WorkerService,service.version=1.0.0,deployment.environment=development"
"OTEL_RESOURCE_ATTRIBUTES": "service.name=WorkerService,service.version=1.0.0,deployment.environment=development",
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion examples/Example.Elastic.OpenTelemetry.Worker/Worker.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ public class Worker(ILogger<Worker> logger) : BackgroundService
private readonly ILogger<Worker> _logger = logger;

private static readonly HttpClient HttpClient = new();
private const string ActivitySourceName = "CustomActivitySource";
public const string ActivitySourceName = "CustomActivitySource";
private static readonly ActivitySource ActivitySource = new(ActivitySourceName, "1.0.0");
private static readonly Meter Meter = new("CustomMeter");
private static readonly Counter<int> Counter = Meter.CreateCounter<int>("invocations",
Expand Down
5 changes: 1 addition & 4 deletions examples/Example.Elastic.OpenTelemetry/Usage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,8 @@ public static async Task BasicBuilderUsageAsync()
{
// NOTE: This sample assumes ENV VARs have been set to configure the Endpoint and Authorization header.

// The simplest scenario with has default listeners and the OTLP exporter.
//using var agent = Agent.Build();

// Build an agent by creating and using an agent builder, adding a single source (for traces and metrics) defined in this sample application.
using var agent = new AgentBuilder(ActivitySourceName).Build();
await using var agent = new AgentBuilder(ActivitySourceName).Build();

// Build an agent by creating and using an agent builder, adding a single source for the app just to the tracer.
//using var agent = new AgentBuilder().AddTracerSource(ActivitySourceName).Build();
Expand Down
111 changes: 2 additions & 109 deletions src/Elastic.OpenTelemetry/Agent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,133 +2,26 @@
// 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.Reflection;
using Microsoft.Extensions.Logging;

using static Elastic.OpenTelemetry.Diagnostics.ElasticOpenTelemetryDiagnostics;

namespace Elastic.OpenTelemetry;

/// <summary>
/// Supports building and accessing an <see cref="IAgent"/> which collects and ships observability signals.
/// </summary>
public static partial class Agent
public static class Agent
{
private static readonly object Lock = new();
private static IAgent? CurrentAgent;

static Agent()
{
var assemblyInformationalVersion = typeof(Agent).Assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion;
InformationalVersion = ParseAssemblyInformationalVersion(assemblyInformationalVersion);
}

/// <summary>
/// Returns the singleton <see cref="IAgent"/> instance.
/// </summary>
/// <remarks>
/// If an instance is not already initialized, this will create and return a
/// default <see cref="IAgent"/> configured with recommended Elastic defaults.
/// </remarks>
public static IAgent Current
{
get
{
if (CurrentAgent is not null)
return CurrentAgent;

lock (Lock)
{
// disable to satisfy double check lock pattern analyzer
// ReSharper disable once InvertIf
if (CurrentAgent is null)
{
var agent = new AgentBuilder().Build();
CurrentAgent = agent;
}
return CurrentAgent;
}
}
}

internal static string InformationalVersion { get; }

/// <summary>
/// Builds an <see cref="IAgent"/>.
/// </summary>
/// <returns>An <see cref="IAgent"/> instance.</returns>
/// <exception cref="Exception">
/// An exception will be thrown if <see cref="Build"/>
/// is called more than once during the lifetime of an application.
/// </exception>
public static IAgent Build(Action<AgentBuilder>? configuration = null)
{
CheckCurrent();

lock (Lock)
{
CheckCurrent();
var agentBuilder = new AgentBuilder();
configuration?.Invoke(agentBuilder);
var agent = agentBuilder.Build();
CurrentAgent = agent;
return CurrentAgent;
}

static void CheckCurrent()
{
if (CurrentAgent is not null)
{
Log(AgentBuildCalledMultipleTimesEvent);
throw new Exception();
}
}
}

internal const string BuildErrorMessage = $"{nameof(Agent)}.{nameof(Build)} called twice or after " +
$"{nameof(Agent)}.{nameof(Current)} was accessed.";

internal const string SetAgentErrorMessage = $"{nameof(Agent)}.{nameof(SetAgent)} called twice" +
$"or after {nameof(Agent)}.{nameof(Build)} or after {nameof(Agent)}.{nameof(Current)} was accessed.";

[LoggerMessage(EventId = 1, Level = LogLevel.Error, Message = SetAgentErrorMessage)]
internal static partial void SetAgentError(this ILogger logger);

/// <summary>
/// TODO
/// </summary>
/// <param name="agent"></param>
/// <param name="logger"></param>
/// <returns></returns>
/// <exception cref="Exception"></exception>
internal static IAgent SetAgent(IAgent agent, ILogger logger)
{
CheckCurrent(logger);

lock (Lock)
{
CheckCurrent(logger);
logger.LogInformation($"Setting {nameof(CurrentAgent)}.");
CurrentAgent = agent;
return CurrentAgent;
}

static void CheckCurrent(ILogger logger)
{
if (CurrentAgent is not null)
{
Log(AgentSetAgentCalledMultipleTimesEvent);
logger.SetAgentError();
throw new Exception(SetAgentErrorMessage);
}
}
}

internal static string ParseAssemblyInformationalVersion(string? informationalVersion)
private static string ParseAssemblyInformationalVersion(string? informationalVersion)
{
if (string.IsNullOrWhiteSpace(informationalVersion))
{
informationalVersion = "1.0.0";
}

/*
* InformationalVersion will be in the following format:
Expand Down
119 changes: 67 additions & 52 deletions src/Elastic.OpenTelemetry/AgentBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
// 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;
using Elastic.OpenTelemetry.DependencyInjection;
using Elastic.OpenTelemetry.Diagnostics;
using Elastic.OpenTelemetry.Diagnostics.Logging;
using Elastic.OpenTelemetry.Extensions;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
Expand All @@ -13,8 +13,6 @@
using OpenTelemetry.Resources;
using OpenTelemetry.Trace;

using static Elastic.OpenTelemetry.Diagnostics.ElasticOpenTelemetryDiagnostics;

namespace Elastic.OpenTelemetry;

/// <summary>
Expand All @@ -28,25 +26,26 @@ public class AgentBuilder
private Action<ResourceBuilder>? _resourceBuilderAction = rb => { };
private Action<OtlpExporterOptions>? _otlpExporterConfiguration;
private string? _otlpExporterName;
private readonly IDisposable? _diagnosticSourceSubscription;

private readonly AgentCompositeLogger _logger;
private bool _skipOtlpRegistration;
private readonly LoggingEventListener _loggingEventListener;

/// <summary>
/// TODO
/// </summary>
public AgentBuilder()
public AgentBuilder(ILogger? logger = null)
{
if (LogFileWriter.FileLoggingEnabled)
{
// Enables logging of OpenTelemetry-SDK event source events
_ = new LoggingEventListener(LogFileWriter.Instance);
_logger = new AgentCompositeLogger(logger);

// Enables logging of Elastic OpenTelemetry diagnostic source events
_diagnosticSourceSubscription = EnableFileLogging();
}
// Enables logging of OpenTelemetry-SDK event source events
_loggingEventListener = new LoggingEventListener(_logger);

Log(AgentBuilderInitializedEvent, () => new DiagnosticEvent<StackTrace?>(new StackTrace(true)));
_logger.LogAgentPreamble();
_logger.LogAgentBuilderInitialized(Environment.NewLine, new StackTrace(true));
}


// NOTE - Applies to all signals
/// <summary>
/// TODO
Expand Down Expand Up @@ -226,23 +225,22 @@ private AgentBuilder TracerInternal(Action<ResourceBuilder>? configureResourceBu
/// Build an instance of <see cref="IAgent"/>.
/// </summary>
/// <returns>A new instance of <see cref="IAgent"/>.</returns>
public IAgent Build()
public IAgent Build(ILogger? logger = null)
{
_logger.SetAdditionalLogger(logger);
var tracerProviderBuilder = Sdk.CreateTracerProviderBuilder();
TracerProviderBuilderAction.Invoke(tracerProviderBuilder);
var tracerProvider = tracerProviderBuilder.Build();

Log(AgentBuilderBuiltTracerProviderEvent);
_logger.LogAgentBuilderBuiltTracerProvider();

var meterProviderBuilder = Sdk.CreateMeterProviderBuilder();
MeterProviderBuilderAction.Invoke(meterProviderBuilder);
var meterProvider = meterProviderBuilder.Build();
_logger.LogAgentBuilderBuiltMeterProvider();

Log(AgentBuilderBuiltMeterProviderEvent);

var agent = new Agent(_diagnosticSourceSubscription, tracerProvider, meterProvider);
var agent = new Agent(_logger, _loggingEventListener, tracerProvider, meterProvider);

Log(AgentBuilderBuiltAgentEvent);
_logger.LogAgentBuilderBuiltAgent();

return agent;
}
Expand All @@ -259,46 +257,54 @@ public IServiceCollection Register(IServiceCollection serviceCollection)

_ = serviceCollection
.AddHostedService<ElasticOtelDistroService>()
// This is purely to register an instance of the agent such that should the service provider be disposed, the agent
// will also be disposed which in turn avoids further diagnostics subscriptions and file logging.
.AddSingleton<IAgent>(new Agent(_diagnosticSourceSubscription))
.AddSingleton<LoggerResolver>()
.AddOpenTelemetry()
.WithTracing(TracerProviderBuilderAction)
.WithMetrics(MeterProviderBuilderAction);

Log(AgentBuilderRegisteredDistroServicesEvent);
_logger.LogAgentBuilderRegisteredServices();

return serviceCollection;
}

/// <summary> TODO </summary>
public AgentBuilder SkipOtlpExporter()
{
_skipOtlpRegistration = true;
return this;
}


private Action<TracerProviderBuilder> TracerProviderBuilderAction =>
tracerProviderBuilder =>
{
foreach (var source in _activitySourceNames)
tracerProviderBuilder.LogAndAddSource(source);
tracerProviderBuilder.LogAndAddSource(source, _logger);
tracerProviderBuilder
.AddHttpClientInstrumentation()
.AddGrpcClientInstrumentation()
.AddEntityFrameworkCoreInstrumentation(); // TODO - Should we add this by default?
tracerProviderBuilder.AddElasticProcessors();
tracerProviderBuilder.AddElasticProcessors(_logger);
var action = _resourceBuilderAction;
action += b => b.AddDistroAttributes();
tracerProviderBuilder.ConfigureResource(action);
_tracerProviderBuilderAction?.Invoke(tracerProviderBuilder);
tracerProviderBuilder.AddOtlpExporter(_otlpExporterName, _otlpExporterConfiguration);
if (!_skipOtlpRegistration)
tracerProviderBuilder.AddOtlpExporter(_otlpExporterName, _otlpExporterConfiguration);
};

private Action<MeterProviderBuilder> MeterProviderBuilderAction =>
builder =>
{
foreach (var source in _activitySourceNames)
builder.LogAndAddMeter(source);
{
_logger.LogMeterAdded(source, builder.GetType().Name);
builder.AddMeter(source);
}
builder
.AddProcessInstrumentation()
Expand Down Expand Up @@ -328,36 +334,45 @@ public void ConfigureOtlpExporter(Action<OtlpExporterOptions> configure, string?
_otlpExporterName = name;
}

private class Agent(IDisposable? diagnosticSubscription, TracerProvider? tracerProvider, MeterProvider? meterProvider) : IAgent
private class Agent(
AgentCompositeLogger logger,
LoggingEventListener loggingEventListener,
TracerProvider? tracerProvider = null,
MeterProvider? meterProvider = null
) : IAgent
{
private readonly IDisposable? _diagnosticSubscription = diagnosticSubscription;
private readonly TracerProvider? _tracerProvider = tracerProvider;
private readonly MeterProvider? _meterProvider = meterProvider;

public Agent(IDisposable? diagnosticSubscription)
: this(diagnosticSubscription,null, null)
{
}

internal Agent(IDisposable? diagnosticSubscription, TracerProvider tracerProvider)
: this(diagnosticSubscription, tracerProvider, null)
{
}

public void Dispose()
{
_tracerProvider?.Dispose();
_meterProvider?.Dispose();
_diagnosticSubscription?.Dispose();
LogFileWriter.Instance.Dispose();
tracerProvider?.Dispose();
meterProvider?.Dispose();
loggingEventListener.Dispose();
logger.Dispose();
}

public async ValueTask DisposeAsync()
{
_tracerProvider?.Dispose();
_meterProvider?.Dispose();
_diagnosticSubscription?.Dispose();
await LogFileWriter.Instance.DisposeAsync().ConfigureAwait(false);
tracerProvider?.Dispose();
meterProvider?.Dispose();
await loggingEventListener.DisposeAsync().ConfigureAwait(false);
await logger.DisposeAsync().ConfigureAwait(false);
}
}
}

internal static partial class LoggerMessages
{
[LoggerMessage(EventId = 0, Level = LogLevel.Trace, Message = $"AgentBuilder initialized{{newline}}{{StackTrace}}.")]
public static partial void LogAgentBuilderInitialized(this ILogger logger, string newline, StackTrace stackTrace);

[LoggerMessage(EventId = 0, Level = LogLevel.Trace, Message = "AgentBuilder built TracerProvider.")]
public static partial void LogAgentBuilderBuiltTracerProvider(this ILogger logger);

[LoggerMessage(EventId = 0, Level = LogLevel.Trace, Message = "AgentBuilder built MeterProvider.")]
public static partial void LogAgentBuilderBuiltMeterProvider(this ILogger logger);

[LoggerMessage(EventId = 0, Level = LogLevel.Trace, Message = "AgentBuilder built Agent.")]
public static partial void LogAgentBuilderBuiltAgent(this ILogger logger);

[LoggerMessage(EventId = 0, Level = LogLevel.Trace, Message = "AgentBuilder registered agent services into IServiceCollection.")]
public static partial void LogAgentBuilderRegisteredServices(this ILogger logger);
}
Loading

0 comments on commit 37ad73e

Please sign in to comment.