From 37ad73e735f22b4ad1cd4457899acbce4346d7a4 Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Mon, 4 Mar 2024 14:00:36 +0100 Subject: [PATCH] Rework logging so everything is ILogger based (#45) * 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) --- .../Program.cs | 2 +- .../Properties/launchSettings.json | 2 +- .../Worker.cs | 2 +- .../Example.Elastic.OpenTelemetry/Usage.cs | 5 +- src/Elastic.OpenTelemetry/Agent.cs | 111 +------ src/Elastic.OpenTelemetry/AgentBuilder.cs | 119 ++++--- .../ElasticOtelDistroService.cs | 22 +- .../DependencyInjection/LoggerResolver.cs | 26 -- .../ServiceCollectionExtensions.cs | 71 +--- .../Diagnostics/DiagnosticEvent.cs | 29 -- .../ElasticDiagnosticLoggingObserver.cs | 140 -------- .../ElasticOpenTelemetryDiagnostics.cs | 125 ------- .../Diagnostics/LogFileWriter.cs | 313 ------------------ .../Diagnostics/LogLevel.cs | 14 - .../Diagnostics/LoggerMessages.cs | 76 ++++- .../Logging/AgentCompositeLogger.cs | 53 +++ .../Logging/AgentLoggingHelpers.cs | 73 ++++ .../Diagnostics/Logging/FileLogger.cs | 123 +++++++ .../Diagnostics/Logging/LogFormatter.cs | 99 ++++++ .../LogLevelHelpers.cs} | 20 +- .../Logging/ScopedCompositeLogger.cs | 52 +++ .../Diagnostics/LoggingEventListener.cs | 27 +- .../Payloads/AddProcessorPayload.cs | 6 - .../Diagnostics/Payloads/AddSourcePayload.cs | 6 - .../EnvironmentVariables.cs | 1 - .../MeterProviderBuilderExtensions.cs | 21 -- .../TracerProviderBuilderExtensions.cs | 20 +- .../Processors/TransactionIdProcessor.cs | 16 +- .../LoggingTests.cs | 75 +++++ .../TransactionIdProcessorTests.cs | 3 +- 30 files changed, 682 insertions(+), 970 deletions(-) delete mode 100644 src/Elastic.OpenTelemetry/DependencyInjection/LoggerResolver.cs delete mode 100644 src/Elastic.OpenTelemetry/Diagnostics/DiagnosticEvent.cs delete mode 100644 src/Elastic.OpenTelemetry/Diagnostics/ElasticDiagnosticLoggingObserver.cs delete mode 100644 src/Elastic.OpenTelemetry/Diagnostics/ElasticOpenTelemetryDiagnostics.cs delete mode 100644 src/Elastic.OpenTelemetry/Diagnostics/LogFileWriter.cs delete mode 100644 src/Elastic.OpenTelemetry/Diagnostics/LogLevel.cs create mode 100644 src/Elastic.OpenTelemetry/Diagnostics/Logging/AgentCompositeLogger.cs create mode 100644 src/Elastic.OpenTelemetry/Diagnostics/Logging/AgentLoggingHelpers.cs create mode 100644 src/Elastic.OpenTelemetry/Diagnostics/Logging/FileLogger.cs create mode 100644 src/Elastic.OpenTelemetry/Diagnostics/Logging/LogFormatter.cs rename src/Elastic.OpenTelemetry/Diagnostics/{DiagnosticErrorLevels.cs => Logging/LogLevelHelpers.cs} (66%) create mode 100644 src/Elastic.OpenTelemetry/Diagnostics/Logging/ScopedCompositeLogger.cs delete mode 100644 src/Elastic.OpenTelemetry/Diagnostics/Payloads/AddProcessorPayload.cs delete mode 100644 src/Elastic.OpenTelemetry/Diagnostics/Payloads/AddSourcePayload.cs delete mode 100644 src/Elastic.OpenTelemetry/Extensions/MeterProviderBuilderExtensions.cs create mode 100644 tests/Elastic.OpenTelemetry.Tests/LoggingTests.cs diff --git a/examples/Example.Elastic.OpenTelemetry.Worker/Program.cs b/examples/Example.Elastic.OpenTelemetry.Worker/Program.cs index c1c8696..97a48a1 100644 --- a/examples/Example.Elastic.OpenTelemetry.Worker/Program.cs +++ b/examples/Example.Elastic.OpenTelemetry.Worker/Program.cs @@ -5,7 +5,7 @@ var builder = Host.CreateApplicationBuilder(args); -builder.AddElasticOpenTelemetry("CustomActivitySource", "CustomMeter"); +builder.Services.AddElasticOpenTelemetry(Worker.ActivitySourceName, "CustomMeter"); builder.Services.AddHostedService(); diff --git a/examples/Example.Elastic.OpenTelemetry.Worker/Properties/launchSettings.json b/examples/Example.Elastic.OpenTelemetry.Worker/Properties/launchSettings.json index fbbfdec..4a93d22 100644 --- a/examples/Example.Elastic.OpenTelemetry.Worker/Properties/launchSettings.json +++ b/examples/Example.Elastic.OpenTelemetry.Worker/Properties/launchSettings.json @@ -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", } } } diff --git a/examples/Example.Elastic.OpenTelemetry.Worker/Worker.cs b/examples/Example.Elastic.OpenTelemetry.Worker/Worker.cs index 3c8b7ad..9f65359 100644 --- a/examples/Example.Elastic.OpenTelemetry.Worker/Worker.cs +++ b/examples/Example.Elastic.OpenTelemetry.Worker/Worker.cs @@ -11,7 +11,7 @@ public class Worker(ILogger logger) : BackgroundService private readonly ILogger _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 Counter = Meter.CreateCounter("invocations", diff --git a/examples/Example.Elastic.OpenTelemetry/Usage.cs b/examples/Example.Elastic.OpenTelemetry/Usage.cs index 85944e0..9df1f14 100644 --- a/examples/Example.Elastic.OpenTelemetry/Usage.cs +++ b/examples/Example.Elastic.OpenTelemetry/Usage.cs @@ -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(); diff --git a/src/Elastic.OpenTelemetry/Agent.cs b/src/Elastic.OpenTelemetry/Agent.cs index 6681a25..74af972 100644 --- a/src/Elastic.OpenTelemetry/Agent.cs +++ b/src/Elastic.OpenTelemetry/Agent.cs @@ -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; /// /// Supports building and accessing an which collects and ships observability signals. /// -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()?.InformationalVersion; InformationalVersion = ParseAssemblyInformationalVersion(assemblyInformationalVersion); } - /// - /// Returns the singleton instance. - /// - /// - /// If an instance is not already initialized, this will create and return a - /// default configured with recommended Elastic defaults. - /// - 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; } - /// - /// Builds an . - /// - /// An instance. - /// - /// An exception will be thrown if - /// is called more than once during the lifetime of an application. - /// - public static IAgent Build(Action? 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); - - /// - /// TODO - /// - /// - /// - /// - /// - 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: diff --git a/src/Elastic.OpenTelemetry/AgentBuilder.cs b/src/Elastic.OpenTelemetry/AgentBuilder.cs index 095f3b2..06be4fc 100644 --- a/src/Elastic.OpenTelemetry/AgentBuilder.cs +++ b/src/Elastic.OpenTelemetry/AgentBuilder.cs @@ -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; @@ -13,8 +13,6 @@ using OpenTelemetry.Resources; using OpenTelemetry.Trace; -using static Elastic.OpenTelemetry.Diagnostics.ElasticOpenTelemetryDiagnostics; - namespace Elastic.OpenTelemetry; /// @@ -28,25 +26,26 @@ public class AgentBuilder private Action? _resourceBuilderAction = rb => { }; private Action? _otlpExporterConfiguration; private string? _otlpExporterName; - private readonly IDisposable? _diagnosticSourceSubscription; + + private readonly AgentCompositeLogger _logger; + private bool _skipOtlpRegistration; + private readonly LoggingEventListener _loggingEventListener; /// /// TODO /// - 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(new StackTrace(true))); + _logger.LogAgentPreamble(); + _logger.LogAgentBuilderInitialized(Environment.NewLine, new StackTrace(true)); } + // NOTE - Applies to all signals /// /// TODO @@ -226,23 +225,22 @@ private AgentBuilder TracerInternal(Action? configureResourceBu /// Build an instance of . /// /// A new instance of . - 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; } @@ -259,31 +257,35 @@ public IServiceCollection Register(IServiceCollection serviceCollection) _ = serviceCollection .AddHostedService() - // 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(new Agent(_diagnosticSourceSubscription)) - .AddSingleton() .AddOpenTelemetry() .WithTracing(TracerProviderBuilderAction) .WithMetrics(MeterProviderBuilderAction); - Log(AgentBuilderRegisteredDistroServicesEvent); + _logger.LogAgentBuilderRegisteredServices(); return serviceCollection; } + /// TODO + public AgentBuilder SkipOtlpExporter() + { + _skipOtlpRegistration = true; + return this; + } + + private Action 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(); @@ -291,14 +293,18 @@ public IServiceCollection Register(IServiceCollection serviceCollection) _tracerProviderBuilderAction?.Invoke(tracerProviderBuilder); - tracerProviderBuilder.AddOtlpExporter(_otlpExporterName, _otlpExporterConfiguration); + if (!_skipOtlpRegistration) + tracerProviderBuilder.AddOtlpExporter(_otlpExporterName, _otlpExporterConfiguration); }; private Action MeterProviderBuilderAction => builder => { foreach (var source in _activitySourceNames) - builder.LogAndAddMeter(source); + { + _logger.LogMeterAdded(source, builder.GetType().Name); + builder.AddMeter(source); + } builder .AddProcessInstrumentation() @@ -328,36 +334,45 @@ public void ConfigureOtlpExporter(Action 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); +} diff --git a/src/Elastic.OpenTelemetry/DependencyInjection/ElasticOtelDistroService.cs b/src/Elastic.OpenTelemetry/DependencyInjection/ElasticOtelDistroService.cs index 27e7aeb..1c32b2e 100644 --- a/src/Elastic.OpenTelemetry/DependencyInjection/ElasticOtelDistroService.cs +++ b/src/Elastic.OpenTelemetry/DependencyInjection/ElasticOtelDistroService.cs @@ -1,28 +1,25 @@ // 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.DependencyInjection; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; namespace Elastic.OpenTelemetry { internal sealed class ElasticOtelDistroService(IServiceProvider serviceProvider) : IHostedService, IHostedLifecycleService { - private readonly IServiceProvider _serviceProvider = serviceProvider; + private IAgent? _agent; public Task StartingAsync(CancellationToken cancellationToken) { - var agent = _serviceProvider.GetRequiredService(); - var loggerFactory = _serviceProvider.GetService(); - var logger = loggerFactory?.CreateLogger($"{nameof(Elastic)}.{nameof(OpenTelemetry)}") ?? NullLogger.Instance; + var loggerFactory = serviceProvider.GetService(); + var logger = loggerFactory?.CreateLogger($"{nameof(Elastic)}.{nameof(OpenTelemetry)}"); - _serviceProvider.GetService(); + _agent = serviceProvider.GetRequiredService().Build(logger); - logger.LogInformation("Initialising Agent.Current."); - Agent.SetAgent(agent, logger); + //logger.LogInformation("Initialising Agent.Current."); + //Agent.SetAgent(_agent, logger); return Task.CompletedTask; } @@ -31,6 +28,11 @@ public Task StartingAsync(CancellationToken cancellationToken) public Task StartedAsync(CancellationToken cancellationToken) => Task.CompletedTask; public Task StoppingAsync(CancellationToken cancellationToken) => Task.CompletedTask; public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; - public Task StoppedAsync(CancellationToken cancellationToken) => Task.CompletedTask; + + public async Task StoppedAsync(CancellationToken cancellationToken) + { + if (_agent != null) + await _agent.DisposeAsync().ConfigureAwait(false); + } } } diff --git a/src/Elastic.OpenTelemetry/DependencyInjection/LoggerResolver.cs b/src/Elastic.OpenTelemetry/DependencyInjection/LoggerResolver.cs deleted file mode 100644 index 04b7c70..0000000 --- a/src/Elastic.OpenTelemetry/DependencyInjection/LoggerResolver.cs +++ /dev/null @@ -1,26 +0,0 @@ -// 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 Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; - -namespace Elastic.OpenTelemetry.DependencyInjection; - -internal sealed class LoggerResolver -{ - private static ILoggerFactory LoggerFactory = NullLoggerFactory.Instance; - - public LoggerResolver(ILoggerFactory loggerFactory) - { - if (LoggerFactory == NullLoggerFactory.Instance) - LoggerFactory = loggerFactory; - } - - internal static ILogger GetLogger() => LoggerFactory.CreateLogger(); - - internal static ILogger GetLogger(ref ILogger? logger) - { - logger = LoggerFactory.CreateLogger(); - return logger; - } -} diff --git a/src/Elastic.OpenTelemetry/DependencyInjection/ServiceCollectionExtensions.cs b/src/Elastic.OpenTelemetry/DependencyInjection/ServiceCollectionExtensions.cs index c571c33..b7db0d2 100644 --- a/src/Elastic.OpenTelemetry/DependencyInjection/ServiceCollectionExtensions.cs +++ b/src/Elastic.OpenTelemetry/DependencyInjection/ServiceCollectionExtensions.cs @@ -13,80 +13,41 @@ namespace Microsoft.Extensions.DependencyInjection; /// public static class ServiceCollectionExtensions { - /// - /// TODO - /// - /// - /// + /// TODO public static IHostApplicationBuilder AddElasticOpenTelemetry(this IHostApplicationBuilder builder) => - AddElasticOpenTelemetry(builder, []); + builder.AddElasticOpenTelemetry([]); - /// - /// TODO - /// - /// - /// - /// + /// public static IHostApplicationBuilder AddElasticOpenTelemetry(this IHostApplicationBuilder builder, params string[] activitySourceNames) { builder.Services.AddElasticOpenTelemetry(activitySourceNames); return builder; } - /// - /// TODO - /// - /// - /// - /// - /// - public static IHostApplicationBuilder AddElasticOpenTelemetry( - this IHostApplicationBuilder builder, - Action? configureTracerProvider, - Action? configureMeterProvider) - { - builder.Services.AddElasticOpenTelemetry(configureTracerProvider, configureMeterProvider); - return builder; - } - /// - /// Adds the Elastic OpenTelemetry distribution to an application via the . - /// - /// TODO - /// TODO - public static IServiceCollection AddElasticOpenTelemetry(this IServiceCollection serviceCollection) => - new AgentBuilder().Register(serviceCollection); - - /// - /// TODO + /// /// /// - /// /// - public static IServiceCollection AddElasticOpenTelemetry(this IServiceCollection serviceCollection, params string[] activitySourceNames) => - new AgentBuilder(activitySourceNames).Register(serviceCollection); + public static AgentBuilder AddElasticOpenTelemetry(this IServiceCollection serviceCollection) => + serviceCollection.AddElasticOpenTelemetry(null); /// /// TODO /// /// - /// - /// + /// /// - public static IServiceCollection AddElasticOpenTelemetry( - this IServiceCollection serviceCollection, - Action? configureTracerProvider, - Action? configureMeterProvider) + public static AgentBuilder AddElasticOpenTelemetry(this IServiceCollection serviceCollection, params string[]? activitySourceNames) { - var builder = new AgentBuilder(); - - if (configureTracerProvider is not null) - builder.ConfigureTracer(configureTracerProvider); - - if (configureMeterProvider is not null) - builder.ConfigureMeter(configureMeterProvider); - - return builder.Register(serviceCollection); + //TODO return IAgentBuilder that does not expose Build() + var builder = new AgentBuilder(activitySourceNames ?? []); + serviceCollection + .AddHostedService() + .AddSingleton(builder) + .AddOpenTelemetry(); + return builder; } + } diff --git a/src/Elastic.OpenTelemetry/Diagnostics/DiagnosticEvent.cs b/src/Elastic.OpenTelemetry/Diagnostics/DiagnosticEvent.cs deleted file mode 100644 index 0452284..0000000 --- a/src/Elastic.OpenTelemetry/Diagnostics/DiagnosticEvent.cs +++ /dev/null @@ -1,29 +0,0 @@ -// 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; -using Elastic.OpenTelemetry.DependencyInjection; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; - -namespace Elastic.OpenTelemetry.Diagnostics; - -internal class DiagnosticEvent(Activity? activity = null, ILogger? logger = null) -{ - public int ManagedThreadId { get; } = Environment.CurrentManagedThreadId; - - public DateTime DateTime { get; } = DateTime.UtcNow; - - public Activity? Activity { get; } = activity ?? Activity.Current; - - public ILogger Logger { get; } = logger ?? NullLogger.Instance; - - public static DiagnosticEvent Create(ref ILogger? logger, Activity? activity = null) => - new(activity, logger ?? LoggerResolver.GetLogger(ref logger)); -} - -internal class DiagnosticEvent(T data, Activity? activity = null, ILogger? logger = null) : DiagnosticEvent(activity, logger) -{ - public T Data { get; init; } = data; -} - diff --git a/src/Elastic.OpenTelemetry/Diagnostics/ElasticDiagnosticLoggingObserver.cs b/src/Elastic.OpenTelemetry/Diagnostics/ElasticDiagnosticLoggingObserver.cs deleted file mode 100644 index d0b4159..0000000 --- a/src/Elastic.OpenTelemetry/Diagnostics/ElasticDiagnosticLoggingObserver.cs +++ /dev/null @@ -1,140 +0,0 @@ -// 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; - -namespace Elastic.OpenTelemetry.Diagnostics; - -internal sealed class ElasticDiagnosticLoggingObserver(LogFileWriter logFileWriter) : IObserver> -{ - private readonly LogFileWriter _logFileWriter = logFileWriter; - - public void OnNext(KeyValuePair data) - { - if (data.Value is not DiagnosticEvent) - return; - - switch (data.Key) - { - case ElasticOpenTelemetryDiagnostics.AgentBuilderInitializedEvent: - AgentBuilderInitialized(data); - break; - - case ElasticOpenTelemetryDiagnostics.AgentBuilderBuiltTracerProviderEvent: - AgentBuilderBuiltTracerProvider(data); - break; - - case ElasticOpenTelemetryDiagnostics.AgentBuilderBuiltMeterProviderEvent: - AgentBuilderBuiltMeterProvider(data); - break; - - case ElasticOpenTelemetryDiagnostics.AgentBuilderBuiltAgentEvent: - AgentBuilderBuiltAgent(data); - break; - - case ElasticOpenTelemetryDiagnostics.AgentBuildCalledMultipleTimesEvent: - AgentBuilderBuildCalledMultipleTimes(data); - break; - - case ElasticOpenTelemetryDiagnostics.AgentSetAgentCalledMultipleTimesEvent: - AgentBuilderSetAgentCalledMultipleTimes(data); - break; - - case ElasticOpenTelemetryDiagnostics.AgentBuilderRegisteredDistroServicesEvent: - AgentBuilderRegisteredDistroServices(data); - break; - - case ElasticOpenTelemetryDiagnostics.TransactionIdAddedEvent: - TransactionIdAdded(data); - break; - - case ElasticOpenTelemetryDiagnostics.ProcessorAddedEvent: - ProcessorAdded(data); - break; - - case ElasticOpenTelemetryDiagnostics.SourceAddedEvent: - SourceAdded(data); - break; - - case ElasticOpenTelemetryDiagnostics.MeterAddedEvent: - MeterAdded(data); - break; - - default: - if (data.Value is DiagnosticEvent diagnostic) - _logFileWriter.LogUnhandledEvent(data.Key, diagnostic); - break; - } - - void AgentBuilderInitialized(KeyValuePair data) - { - if (data.Value is DiagnosticEvent diagnostic) - _logFileWriter.LogAgentBuilderInitialized(diagnostic); - } - - void AgentBuilderBuiltTracerProvider(KeyValuePair data) - { - if (data.Value is DiagnosticEvent diagnostic) - _logFileWriter.LogAgentBuilderBuiltTracerProvider(diagnostic); - } - - void AgentBuilderBuiltMeterProvider(KeyValuePair data) - { - if (data.Value is DiagnosticEvent diagnostic) - _logFileWriter.LogAgentBuilderBuiltMeterProvider(diagnostic); - } - - void AgentBuilderBuiltAgent(KeyValuePair data) - { - if (data.Value is DiagnosticEvent diagnostic) - _logFileWriter.LogAgentBuilderBuiltAgent(diagnostic); - } - - void AgentBuilderRegisteredDistroServices(KeyValuePair data) - { - if (data.Value is DiagnosticEvent diagnostic) - _logFileWriter.LogAgentBuilderRegisteredServices(diagnostic); - } - - void AgentBuilderBuildCalledMultipleTimes(KeyValuePair data) - { - if (data.Value is DiagnosticEvent diagnostic) - _logFileWriter.LogAgentBuilderBuildCalledMultipleTimes(diagnostic); - } - - void AgentBuilderSetAgentCalledMultipleTimes(KeyValuePair data) - { - if (data.Value is DiagnosticEvent diagnostic) - _logFileWriter.LogAgentBuilderSetAgentCalledMultipleTimes(diagnostic); - } - - void TransactionIdAdded(KeyValuePair data) - { - if (data.Value is DiagnosticEvent diagnostic) - _logFileWriter.LogAddedTransactionIdTag(diagnostic); - } - - void ProcessorAdded(KeyValuePair data) - { - if (data.Value is DiagnosticEvent diagnostic) - _logFileWriter.LogProcessorAdded(diagnostic); - } - - void SourceAdded(KeyValuePair data) - { - if (data.Value is DiagnosticEvent diagnostic) - _logFileWriter.LogSourceAdded(diagnostic); - } - - void MeterAdded(KeyValuePair data) - { - if (data.Value is DiagnosticEvent diagnostic) - _logFileWriter.LogMeterAdded(diagnostic); - } - } - - public void OnCompleted() { } - - public void OnError(Exception error) { } -} - diff --git a/src/Elastic.OpenTelemetry/Diagnostics/ElasticOpenTelemetryDiagnostics.cs b/src/Elastic.OpenTelemetry/Diagnostics/ElasticOpenTelemetryDiagnostics.cs deleted file mode 100644 index 539c51f..0000000 --- a/src/Elastic.OpenTelemetry/Diagnostics/ElasticOpenTelemetryDiagnostics.cs +++ /dev/null @@ -1,125 +0,0 @@ -// 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; -using Elastic.OpenTelemetry.DependencyInjection; -using Microsoft.Extensions.Logging.Abstractions; - -namespace Elastic.OpenTelemetry.Diagnostics; - -internal static class ElasticOpenTelemetryDiagnostics -{ - private static readonly DiagnosticListener Listener = new(DiagnosticSourceName); - - public const string DiagnosticSourceName = "Elastic.OpenTelemetry"; - - internal static readonly DiagnosticSource DiagnosticSource = Listener; - - public static IDisposable EnableFileLogging() => - Listener.Subscribe(new ElasticDiagnosticLoggingObserver(LogFileWriter.Instance)); - - public static void Log(string name) - { - if (DiagnosticSource.IsEnabled(name)) - DiagnosticSource.Write(name, new DiagnosticEvent()); - } - - public static void Log(string name, Func createDiagnosticEvent) - { - // We take a func here so that we only create an instance of the DiagnosticEvent when - // there is a listener for the event. - - if (DiagnosticSource.IsEnabled(name) && createDiagnosticEvent is not null) - DiagnosticSource.Write(name, createDiagnosticEvent.Invoke()); - } - - // Events - - public const string AgentBuilderInitializedEvent = "AgentBuilderInitialized"; - - public const string AgentBuilderBuiltTracerProviderEvent = "AgentBuilderBuiltTracerProvider"; - - public const string AgentBuilderBuiltMeterProviderEvent = "AgentBuilderBuiltMeterProvider"; - - public const string AgentBuilderRegisteredDistroServicesEvent = "RegisteredDistroServices"; - - public const string AgentBuilderBuiltAgentEvent = "AgentBuilderBuiltAgent"; - - public const string TransactionIdAddedEvent = "TransactionIdAdded"; - - public const string ProcessorAddedEvent = "ProcessorAdded"; - - public const string SourceAddedEvent = "SourceAdded"; - - public const string MeterAddedEvent = "MeterAdded"; - - public const string AgentBuildCalledMultipleTimesEvent = "AgentBuildCalledMultipleTimes"; - - public const string AgentSetAgentCalledMultipleTimesEvent = "AgentSetAgentCalledMultipleTimes"; - - public static void LogAgentBuilderInitialized(this LogFileWriter logFileWriter, DiagnosticEvent diagnostic) - { - var message = diagnostic.Data is not null - ? $"AgentBuilder initialized{Environment.NewLine}{diagnostic.Data}." - : "AgentBuilder initialized."; - - logFileWriter.WriteInfoLogLine(diagnostic, message); - } - - public static void LogAgentBuilderBuiltTracerProvider(this LogFileWriter logFileWriter, DiagnosticEvent diagnostic) => - logFileWriter.WriteInfoLogLine(diagnostic, "AgentBuilder built TracerProvider."); - - public static void LogAgentBuilderBuiltMeterProvider(this LogFileWriter logFileWriter, DiagnosticEvent diagnostic) => - logFileWriter.WriteInfoLogLine(diagnostic, "AgentBuilder built MeterProvider."); - - public static void LogAgentBuilderBuiltAgent(this LogFileWriter logFileWriter, DiagnosticEvent diagnostic) => - logFileWriter.WriteInfoLogLine(diagnostic, "AgentBuilder built Agent."); - - public static void LogAgentBuilderRegisteredServices(this LogFileWriter logFileWriter, DiagnosticEvent diagnostic) => - logFileWriter.WriteInfoLogLine(diagnostic, "AgentBuilder registered agent services into IServiceCollection."); - - public static void LogAgentBuilderBuildCalledMultipleTimes(this LogFileWriter logFileWriter, DiagnosticEvent diagnostic) => - logFileWriter.WriteErrorLogLine(diagnostic, Agent.BuildErrorMessage); - - public static void LogAgentBuilderSetAgentCalledMultipleTimes(this LogFileWriter logFileWriter, DiagnosticEvent diagnostic) => - logFileWriter.WriteErrorLogLine(diagnostic, Agent.SetAgentErrorMessage); - - public static void LogAddedTransactionIdTag(this LogFileWriter logFileWriter, DiagnosticEvent diagnostic) - { - diagnostic.Logger.TransactionIdProcessorTagAdded(); - logFileWriter.WriteTraceLogLine(diagnostic, LoggerMessages.TransactionIdProcessorTagAddedLog); - } - - public static void LogProcessorAdded(this LogFileWriter logFileWriter, DiagnosticEvent diagnostic) - { - var message = $"Added '{diagnostic.Data.ProcessorType}' processor to '{diagnostic.Data.BuilderType.Name}'."; - logFileWriter.WriteInfoLogLine(diagnostic, message); - } - - public static void LogSourceAdded(this LogFileWriter logFileWriter, DiagnosticEvent diagnostic) - { - var message = $"Added '{diagnostic.Data.Name}' ActivitySource to '{diagnostic.Data.BuilderType.Name}'."; - logFileWriter.WriteInfoLogLine(diagnostic, message); - } - - public static void LogMeterAdded(this LogFileWriter logFileWriter, DiagnosticEvent diagnostic) - { - var message = $"Added '{diagnostic.Data.Name}' Meter to '{diagnostic.Data.BuilderType.Name}'."; - logFileWriter.WriteInfoLogLine(diagnostic, message); - } - - public static void LogUnhandledEvent(this LogFileWriter logFileWriter, string eventKey, DiagnosticEvent diagnostic) - { - // Prefer the logger from the source of the event, when present, otherwise - // fallback to using a logger typed to the ElasticDiagnosticLoggingObserver instead. - - var logger = diagnostic.Logger; - - if (logger == NullLogger.Instance) - logger = LoggerResolver.GetLogger(); - - logger.UnhandledDiagnosticEvent(eventKey); - - logFileWriter.WriteWarningLogLine(diagnostic, $"Received an unhandled diagnostic event '{eventKey}'."); - } -} diff --git a/src/Elastic.OpenTelemetry/Diagnostics/LogFileWriter.cs b/src/Elastic.OpenTelemetry/Diagnostics/LogFileWriter.cs deleted file mode 100644 index 2cfc5d6..0000000 --- a/src/Elastic.OpenTelemetry/Diagnostics/LogFileWriter.cs +++ /dev/null @@ -1,313 +0,0 @@ -// 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; -using System.Runtime.InteropServices; -using System.Text; -using System.Threading.Channels; - -namespace Elastic.OpenTelemetry.Diagnostics; - -internal sealed class LogFileWriter : IDisposable, IAsyncDisposable -{ - public static readonly bool FileLoggingEnabled = IsFileLoggingEnabled(); - - private bool _disposing; - private readonly ManualResetEventSlim _syncDisposeWaitHandle = new(false); - private readonly StreamWriter _streamWriter; - private readonly Channel _channel = Channel.CreateBounded(new BoundedChannelOptions(1024) - { - SingleReader = true, - SingleWriter = false, - AllowSynchronousContinuations = true, - FullMode = BoundedChannelFullMode.Wait - }); - - private LogFileWriter() - { - var process = Process.GetCurrentProcess(); - - var configuredPath = Environment.GetEnvironmentVariable(EnvironmentVariables.ElasticOtelLogDirectoryEnvironmentVariable); - - // Defaults to local application data (C:\Users\{username}\AppData\Local\Elastic on Windows; $HOME/.local/share/elastic on Linux) - var path = string.IsNullOrEmpty(configuredPath) - ? Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), GetElasticFolder) - : configuredPath; - - // When ordered by filename, we get see logs from the same process grouped, then ordered by oldest to newest, then the PID for that instance - LogFilePath = Path.Combine(path, $"{process.ProcessName}_{DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()}_{process.Id}.agent.log"); - - if (!Directory.Exists(path)) - Directory.CreateDirectory(path); - - _streamWriter = new StreamWriter(LogFilePath, Encoding.UTF8, new FileStreamOptions - { - Access = FileAccess.Write, - Mode = FileMode.OpenOrCreate, - }); - - WritingTask = Task.Run(async () => - { - while (await _channel.Reader.WaitToReadAsync().ConfigureAwait(false) && !_disposing) - while (_channel.Reader.TryRead(out var logLine) && !_disposing) - await _streamWriter.WriteLineAsync(logLine).ConfigureAwait(false); - - _syncDisposeWaitHandle.Set(); - }); - - var builder = StringBuilderCache.Acquire(); - - // Preamble and configuration entries are ALWAYS logged, regardless of the configured log level - LogPreamble(process, builder, _streamWriter); - LogConfiguration(builder, _streamWriter); - - StringBuilderCache.Release(builder); - - _streamWriter.AutoFlush = true; // Ensure we don't lose logs by not flushing to the file. - - static void LogPreamble(Process process, StringBuilder stringBuilder, StreamWriter streamWriter) - { - string[] preAmble = [ - $"Elastic OpenTelemetry Distribution: {Agent.InformationalVersion}", - $"Process ID: {process.Id}", - $"Process name: {process.ProcessName}", - $"Process path: {Environment.ProcessPath}", - $"Process started: {process.StartTime.ToUniversalTime():yyyy-MM-dd HH:mm:ss.fff}", - $"Machine name: {Environment.MachineName}", - $"Process username: {Environment.UserName}", - $"User domain name: {Environment.UserDomainName}", - $"Command line: {Environment.CommandLine}", - $"Command current directory: {Environment.CurrentDirectory}", - $"Processor count: {Environment.ProcessorCount}", - $"OS version: {Environment.OSVersion}", - $"CLR version: {Environment.Version}", - ]; - - foreach (var item in preAmble) - { - WriteLogPrefix(LogLevel.Info, stringBuilder); - stringBuilder.Append(item); - streamWriter.WriteLine(stringBuilder.ToString()); - stringBuilder.Clear(); - } - - streamWriter.Flush(); - } - - static void LogConfiguration(StringBuilder stringBuilder, StreamWriter streamWriter) - { - string[] environmentVariables = [ - EnvironmentVariables.ElasticOtelFileLogging, - EnvironmentVariables.ElasticOtelLogDirectoryEnvironmentVariable, - EnvironmentVariables.ElasticOtelLogLevelEnvironmentVariable - ]; - - foreach (var variable in environmentVariables) - { - var envVarValue = Environment.GetEnvironmentVariable(variable); - - WriteLogPrefix(LogLevel.Info, stringBuilder); - - if (!string.IsNullOrEmpty(envVarValue)) - { - stringBuilder.Append($"Environment variable '{variable}' = '{envVarValue}'."); - streamWriter.WriteLine(stringBuilder.ToString()); - } - else - { - stringBuilder.Append($"Environment variable '{variable}' is not configured."); - streamWriter.WriteLine(stringBuilder.ToString()); - } - - stringBuilder.Clear(); - } - - streamWriter.Flush(); - } - } - - private static readonly LogLevel ConfiguredLogLevel = GetConfiguredLogLevel(); - - public static LogLevel GetConfiguredLogLevel() - { - var logLevel = LogLevel.Info; - - var logLevelEnvironmentVariable = Environment.GetEnvironmentVariable(EnvironmentVariables.ElasticOtelLogLevelEnvironmentVariable); - - if (!string.IsNullOrEmpty(logLevelEnvironmentVariable)) - { - if (logLevelEnvironmentVariable.Equals(DiagnosticErrorLevels.Trace, StringComparison.OrdinalIgnoreCase)) - logLevel = LogLevel.Trace; - - else if (logLevelEnvironmentVariable.Equals(DiagnosticErrorLevels.Info, StringComparison.OrdinalIgnoreCase)) - logLevel = LogLevel.Info; - - else if (logLevelEnvironmentVariable.Equals(DiagnosticErrorLevels.Warning, StringComparison.OrdinalIgnoreCase)) - logLevel = LogLevel.Warning; - - else if (logLevelEnvironmentVariable.Equals(DiagnosticErrorLevels.Error, StringComparison.OrdinalIgnoreCase)) - logLevel = LogLevel.Error; - - else if (logLevelEnvironmentVariable.Equals(DiagnosticErrorLevels.Critical, StringComparison.OrdinalIgnoreCase)) - logLevel = LogLevel.Critical; - } - - return logLevel; - } - - private static void WriteLogPrefix(LogLevel logLevel, StringBuilder builder) => - WriteLogPrefix(Environment.CurrentManagedThreadId, DateTime.UtcNow, logLevel, builder); - - private const string EmptySpanId = "------"; - - private static void WriteLogPrefix(int managedThreadId, DateTime dateTime, LogLevel level, StringBuilder builder, string spanId = "") - { - const int maxLength = 5; - - if (string.IsNullOrEmpty(spanId)) - spanId = EmptySpanId; - - var threadId = new string('-', maxLength); - - if (managedThreadId > 0) - { - var digits = (int)Math.Floor(Math.Log10(managedThreadId) + 1); - - if (digits < 5) - { - Span buffer = stackalloc char[maxLength]; - for (var i = 0; i < maxLength - digits; i++) - { - buffer[i] = '0'; - } - managedThreadId.TryFormat(buffer[(maxLength - digits)..], out _); - threadId = buffer.ToString(); - } - else - { - threadId = managedThreadId.ToString(); - } - } - - builder.Append('[') - .Append(dateTime.ToString("yyyy-MM-dd HH:mm:ss.fff")) - .Append("][") - .Append(threadId) - .Append("][") - .Append(spanId[..6]) - .Append("][") - .Append(level.AsString()) - .Append(']'); - - var length = builder.Length; - var padding = 52 - length; - - for (var i = 0; i < padding; i++) - { - builder.Append(' '); - } - } - - public static LogFileWriter Instance { get; } = new(); - - public string LogFilePath { get; } - - public Task WritingTask { get; } - - public void WriteCriticalLogLine(DiagnosticEvent diagnosticEvent, string message) => - WriteLogLine(diagnosticEvent, LogLevel.Critical, message); - - public void WriteErrorLogLine(DiagnosticEvent diagnosticEvent, string message) => - WriteLogLine(diagnosticEvent, LogLevel.Error, message); - - public void WriteWarningLogLine(DiagnosticEvent diagnosticEvent, string message) => - WriteLogLine(diagnosticEvent, LogLevel.Warning, message); - - public void WriteInfoLogLine(DiagnosticEvent diagnosticEvent, string message) => - WriteLogLine(diagnosticEvent, LogLevel.Info, message); - - public void WriteTraceLogLine(DiagnosticEvent diagnosticEvent, string message) => - WriteLogLine(diagnosticEvent, LogLevel.Trace, message); - - public void WriteLogLine(DiagnosticEvent diagnosticEvent, LogLevel logLevel, string message) => - WriteLogLine(diagnosticEvent.Activity, diagnosticEvent.ManagedThreadId, diagnosticEvent.DateTime, logLevel, message); - - public void WriteLogLine(Activity? activity, int managedThreadId, DateTime dateTime, LogLevel logLevel, string logLine) => - WriteLogLine(activity, managedThreadId, dateTime, logLevel, logLine, null); - - public void WriteLogLine(Activity? activity, int managedThreadId, DateTime dateTime, LogLevel logLevel, string logLine, string? spanId) - { - // We skip logging for any log level higher (numerically) than the configured log level - if (logLevel > ConfiguredLogLevel) - return; - - var builder = StringBuilderCache.Acquire(); - WriteLogPrefix(managedThreadId, dateTime, logLevel, builder, spanId ?? activity?.SpanId.ToHexString() ?? string.Empty); - builder.Append(logLine); - - if (activity is not null) - { - // Accessing activity.Id here will cause the Id to be initialized - // before the sampler runs in case where the activity is created using legacy way - // i.e. new Activity("Operation name"). This will result in Id not reflecting the - // correct sampling flags - // https://github.com/dotnet/runtime/issues/61857 - - var activityId = $"00-{activity.TraceId.ToHexString()}-{activity.SpanId.ToHexString()}-{(activity.ActivityTraceFlags.HasFlag(ActivityTraceFlags.Recorded) ? "01" : "00")}"; - builder.Append($" <{activityId}>"); - } - - var spin = new SpinWait(); - var message = StringBuilderCache.GetStringAndRelease(builder); - while (!_disposing) - { - if (_channel.Writer.TryWrite(message)) - break; - spin.SpinOnce(); - } - } - - private static bool IsFileLoggingEnabled() - { - var enableFileLogging = true; - - var isEnabledValue = Environment.GetEnvironmentVariable(EnvironmentVariables.ElasticOtelFileLogging); - - if (!string.IsNullOrEmpty(isEnabledValue)) - { - if (int.TryParse(isEnabledValue, out var intValue)) - enableFileLogging = intValue == 1; - else if (bool.TryParse(isEnabledValue, out var boolValue)) - enableFileLogging = boolValue; - } - - return enableFileLogging; - } - - private static string GetElasticFolder => - RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "Elastic" : "elastic"; - - public void Dispose() - { - //tag that we are running a dispose this allows running tasks and spin waits to short circuit - _disposing = true; - _channel.Writer.TryComplete(); - - _syncDisposeWaitHandle.Wait(TimeSpan.FromSeconds(1)); - - _streamWriter.Dispose(); - } - - public async ValueTask DisposeAsync() - { - //tag that we are running a dispose this allows running tasks and spin waits to short circuit - _disposing = true; - - _channel.Writer.TryComplete(); - - //Writing task will short circuit once _disposing is flipped to true - await WritingTask.ConfigureAwait(false); - - await _streamWriter.DisposeAsync().ConfigureAwait(false); - } -} diff --git a/src/Elastic.OpenTelemetry/Diagnostics/LogLevel.cs b/src/Elastic.OpenTelemetry/Diagnostics/LogLevel.cs deleted file mode 100644 index 1e64591..0000000 --- a/src/Elastic.OpenTelemetry/Diagnostics/LogLevel.cs +++ /dev/null @@ -1,14 +0,0 @@ -// 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.Diagnostics; - -internal enum LogLevel -{ - Unknown = 0, - Critical = 1, - Error = 2, - Warning = 3, - Info = 4, - Trace = 5 -} diff --git a/src/Elastic.OpenTelemetry/Diagnostics/LoggerMessages.cs b/src/Elastic.OpenTelemetry/Diagnostics/LoggerMessages.cs index 6d03c06..3a47f4c 100644 --- a/src/Elastic.OpenTelemetry/Diagnostics/LoggerMessages.cs +++ b/src/Elastic.OpenTelemetry/Diagnostics/LoggerMessages.cs @@ -1,26 +1,70 @@ // 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; +using Elastic.OpenTelemetry.Diagnostics.Logging; using Elastic.OpenTelemetry.Processors; using Microsoft.Extensions.Logging; -namespace Elastic.OpenTelemetry.Diagnostics +namespace Elastic.OpenTelemetry.Diagnostics; + +internal static partial class LoggerMessages { - internal static partial class LoggerMessages + [LoggerMessage(EventId = 1, Level = LogLevel.Trace, Message = $"{nameof(TransactionIdProcessor)} added 'transaction.id' tag to Activity.")] + internal static partial void TransactionIdProcessorTagAdded(this ILogger logger); + + [LoggerMessage(EventId = 0, Level = LogLevel.Trace, Message = "Added '{ActivitySourceName}' ActivitySource to '{BuilderTypeName}'.")] + public static partial void LogSourceAdded(this ILogger logger, string activitySourceName, string builderTypeName); + + [LoggerMessage(EventId = 0, Level = LogLevel.Trace, Message = "Added '{ProcessorTypeName}' processor to '{BuilderTypeName}'.")] + public static partial void LogProcessorAdded(this ILogger logger, string processorTypeName, string builderTypeName); + + [LoggerMessage(EventId = 0, Level = LogLevel.Trace, Message = "Added '{MeterName}' meter to '{BuilderTypeName}'.")] + public static partial void LogMeterAdded(this ILogger logger, string meterName, string builderTypeName); + + public static void LogAgentPreamble(this ILogger logger) { - public const string TransactionIdProcessorTagAddedLog = - $"{nameof(TransactionIdProcessor)} added 'transaction.id' tag to Activity."; - - [LoggerMessage( - EventId = 1, - Level = Microsoft.Extensions.Logging.LogLevel.Trace, - Message = TransactionIdProcessorTagAddedLog)] - internal static partial void TransactionIdProcessorTagAdded(this ILogger logger); - - [LoggerMessage( - EventId = 100, - Level = Microsoft.Extensions.Logging.LogLevel.Warning, - Message = "Received an unhandled diagnostic event '{EventName}'.")] - internal static partial void UnhandledDiagnosticEvent(this ILogger logger, string eventName); + var process = Process.GetCurrentProcess(); + logger.LogInformation("Elastic OpenTelemetry Distribution: {AgentInformationalVersion}", Agent.InformationalVersion); + if (logger is AgentCompositeLogger agentLogger) + { + if (agentLogger.LogFileEnabled) + logger.LogInformation("Elastic OpenTelemetry Distribution, log file: {LogFilePath}", agentLogger.LogFilePath); + else + logger.LogInformation("Elastic OpenTelemetry Distribution, log file: "); + } + + logger.LogInformation("Process ID: {ProcessId}", process.Id); + logger.LogInformation("Process name: {ProcessName}", process.ProcessName); + logger.LogInformation("Process path: {ProcessPath}", Environment.ProcessPath); + + logger.LogInformation("Process started: {ProcessStartTime:yyyy-MM-dd HH:mm:ss.fff}", process.StartTime.ToUniversalTime()); + logger.LogInformation("Machine name: {MachineName}", Environment.MachineName); + logger.LogInformation("Process username: {UserName}", Environment.UserName); + logger.LogInformation("User domain name: {UserDomainName}", Environment.UserDomainName); + // Don't think we should log this for PII purposes? + //logger.LogInformation("Command line: {ProcessCommandLine}", Environment.CommandLine); + logger.LogInformation("Command current directory: {CurrentDirectory}", Environment.CurrentDirectory); + logger.LogInformation("Processor count: {ProcessorCount}", Environment.ProcessorCount); + logger.LogInformation("OS version: {OSVersion}", Environment.OSVersion); + logger.LogInformation("CLR version: {CLRVersion}", Environment.Version); + + string[] environmentVariables = + [ + EnvironmentVariables.ElasticOtelLogDirectoryEnvironmentVariable, + EnvironmentVariables.ElasticOtelLogLevelEnvironmentVariable + ]; + + foreach (var variable in environmentVariables) + { + var envVarValue = Environment.GetEnvironmentVariable(variable); + + if (string.IsNullOrEmpty(envVarValue)) + logger.LogDebug("Environment variable '{EnvironmentVariable}' is not configured.", variable); + else + logger.LogDebug("Environment variable '{EnvironmentVariable}' = '{EnvironmentVariableValue}'.", variable, envVarValue); + } } + } diff --git a/src/Elastic.OpenTelemetry/Diagnostics/Logging/AgentCompositeLogger.cs b/src/Elastic.OpenTelemetry/Diagnostics/Logging/AgentCompositeLogger.cs new file mode 100644 index 0000000..46abf4d --- /dev/null +++ b/src/Elastic.OpenTelemetry/Diagnostics/Logging/AgentCompositeLogger.cs @@ -0,0 +1,53 @@ +// 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 Microsoft.Extensions.Logging; + +namespace Elastic.OpenTelemetry.Diagnostics.Logging; + +/// A composite logger for use inside the agent, will dispose +internal sealed class AgentCompositeLogger(ILogger? additionalLogger) : IDisposable, IAsyncDisposable, ILogger +{ + public FileLogger FileLogger { get; } = new(); + + /// TODO + public void Dispose() => FileLogger.Dispose(); + + /// TODO + public ValueTask DisposeAsync() => FileLogger.DisposeAsync(); + + /// TODO + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) + { + if (FileLogger.IsEnabled(logLevel)) + FileLogger.Log(logLevel, eventId, state, exception, formatter); + + if (additionalLogger == null) return; + + if (additionalLogger.IsEnabled(logLevel)) + additionalLogger.Log(logLevel, eventId, state, exception, formatter); + } + + public bool LogFileEnabled => FileLogger.FileLoggingEnabled; + public string LogFilePath => FileLogger.LogFilePath ?? string.Empty; + + /// TODO + public void SetAdditionalLogger(ILogger? logger) => additionalLogger ??= logger; + + /// TODO + public bool IsEnabled(LogLevel logLevel) => FileLogger.IsEnabled(logLevel) || (additionalLogger?.IsEnabled(logLevel) ?? false); + + /// TODO + public IDisposable BeginScope(TState state) where TState : notnull => + new CompositeDisposable(FileLogger.BeginScope(state), additionalLogger?.BeginScope(state)); + + private class CompositeDisposable(params IDisposable?[] disposables) : IDisposable + { + public void Dispose() + { + foreach(var disposable in disposables) + disposable?.Dispose(); + } + } +} diff --git a/src/Elastic.OpenTelemetry/Diagnostics/Logging/AgentLoggingHelpers.cs b/src/Elastic.OpenTelemetry/Diagnostics/Logging/AgentLoggingHelpers.cs new file mode 100644 index 0000000..5c2ff1e --- /dev/null +++ b/src/Elastic.OpenTelemetry/Diagnostics/Logging/AgentLoggingHelpers.cs @@ -0,0 +1,73 @@ +// 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; +using Microsoft.Extensions.Logging; + +namespace Elastic.OpenTelemetry.Diagnostics.Logging; + +internal record LogState +{ + public Activity? Activity { get; init; } + public int ManagedThreadId { get; init; } + public DateTime DateTime { get; init; } + public string? SpanId { get; init; } +} + +internal static class AgentLoggingHelpers +{ + public static bool IsFileLoggingEnabled() + { + var logDir = Environment.GetEnvironmentVariable(EnvironmentVariables.ElasticOtelLogDirectoryEnvironmentVariable); + return !string.IsNullOrWhiteSpace(logDir); + } + + public static LogLevel GetElasticOtelLogLevel() + { + var logLevel = LogLevel.Information; + + var logLevelEnvironmentVariable = Environment.GetEnvironmentVariable(EnvironmentVariables.ElasticOtelLogLevelEnvironmentVariable); + + if (!string.IsNullOrEmpty(logLevelEnvironmentVariable)) + { + if (logLevelEnvironmentVariable.Equals(LogLevelHelpers.Trace, StringComparison.OrdinalIgnoreCase)) + logLevel = LogLevel.Trace; + + else if (logLevelEnvironmentVariable.Equals(LogLevelHelpers.Debug, StringComparison.OrdinalIgnoreCase)) + logLevel = LogLevel.Debug; + + else if (logLevelEnvironmentVariable.Equals(LogLevelHelpers.Info, StringComparison.OrdinalIgnoreCase)) + logLevel = LogLevel.Information; + + else if (logLevelEnvironmentVariable.Equals(LogLevelHelpers.Information, StringComparison.OrdinalIgnoreCase)) + logLevel = LogLevel.Information; + + else if (logLevelEnvironmentVariable.Equals(LogLevelHelpers.Warning, StringComparison.OrdinalIgnoreCase)) + logLevel = LogLevel.Warning; + + else if (logLevelEnvironmentVariable.Equals(LogLevelHelpers.Error, StringComparison.OrdinalIgnoreCase)) + logLevel = LogLevel.Error; + + else if (logLevelEnvironmentVariable.Equals(LogLevelHelpers.Critical, StringComparison.OrdinalIgnoreCase)) + logLevel = LogLevel.Critical; + } + + return logLevel; + } + + public static void WriteLogLine(this ILogger logger, Activity? activity, int managedThreadId, DateTime dateTime, LogLevel logLevel, string logLine, string? spanId) + { + var state = new LogState + { + Activity = activity, + ManagedThreadId = managedThreadId, + DateTime = dateTime, + SpanId = spanId + }; + logger.Log(logLevel, 0, state, null, (_, _) => logLine); + } + + + +} diff --git a/src/Elastic.OpenTelemetry/Diagnostics/Logging/FileLogger.cs b/src/Elastic.OpenTelemetry/Diagnostics/Logging/FileLogger.cs new file mode 100644 index 0000000..4722934 --- /dev/null +++ b/src/Elastic.OpenTelemetry/Diagnostics/Logging/FileLogger.cs @@ -0,0 +1,123 @@ +// 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; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading.Channels; +using Microsoft.Extensions.Logging; + +namespace Elastic.OpenTelemetry.Diagnostics.Logging; + +internal sealed class FileLogger : IDisposable, IAsyncDisposable, ILogger +{ + private bool _disposing; + private readonly ManualResetEventSlim _syncDisposeWaitHandle = new(false); + private readonly StreamWriter? _streamWriter; + private readonly Channel _channel = Channel.CreateBounded(new BoundedChannelOptions(1024) + { + SingleReader = true, + SingleWriter = false, + AllowSynchronousContinuations = true, + FullMode = BoundedChannelFullMode.Wait + }); + + private static readonly LogLevel ConfiguredLogLevel = AgentLoggingHelpers.GetElasticOtelLogLevel(); + + public bool FileLoggingEnabled { get; } = AgentLoggingHelpers.IsFileLoggingEnabled(); + + private readonly LoggerExternalScopeProvider _scopeProvider; + + public FileLogger() + { + _scopeProvider = new LoggerExternalScopeProvider(); + var process = Process.GetCurrentProcess(); + + if (!FileLoggingEnabled) return; + + var configuredPath = Environment.GetEnvironmentVariable(EnvironmentVariables.ElasticOtelLogDirectoryEnvironmentVariable); + + // Defaults to local application data (C:\Users\{username}\AppData\Local\Elastic on Windows; $HOME/.local/share/elastic on Linux) + var path = string.IsNullOrEmpty(configuredPath) + ? Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), GetElasticFolder) + : configuredPath; + + // When ordered by filename, we get see logs from the same process grouped, then ordered by oldest to newest, then the PID for that instance + LogFilePath = Path.Combine(path, $"{process.ProcessName}_{DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()}_{process.Id}.agent.log"); + + if (!Directory.Exists(path)) + Directory.CreateDirectory(path); + + _streamWriter = new StreamWriter(LogFilePath, Encoding.UTF8, new FileStreamOptions + { + Access = FileAccess.Write, + Mode = FileMode.OpenOrCreate, + }); + + WritingTask = Task.Run(async () => + { + while (await _channel.Reader.WaitToReadAsync().ConfigureAwait(false) && !_disposing) + while (_channel.Reader.TryRead(out var logLine) && !_disposing) + await _streamWriter.WriteLineAsync(logLine).ConfigureAwait(false); + + _syncDisposeWaitHandle.Set(); + }); + + _streamWriter.AutoFlush = true; // Ensure we don't lose logs by not flushing to the file. + } + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) + { + // We skip logging for any log level higher (numerically) than the configured log level + if (!IsEnabled(logLevel)) + return; + + var logLine = LogFormatter.Format(logLevel, eventId, state, exception, formatter); + + var spin = new SpinWait(); + while (!_disposing) + { + if (_channel.Writer.TryWrite(logLine)) + break; + spin.SpinOnce(); + } + } + + public bool IsEnabled(LogLevel logLevel) => FileLoggingEnabled && ConfiguredLogLevel <= logLevel; + + public IDisposable? BeginScope(TState state) where TState : notnull => _scopeProvider.Push(state); + + public string? LogFilePath { get; } + + public Task? WritingTask { get; } + + private static string GetElasticFolder => RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "Elastic" : "elastic"; + + public void Dispose() + { + //tag that we are running a dispose this allows running tasks and spin waits to short circuit + _disposing = true; + _channel.Writer.TryComplete(); + + _syncDisposeWaitHandle.Wait(TimeSpan.FromSeconds(1)); + + _streamWriter?.Dispose(); + } + + public async ValueTask DisposeAsync() + { + //tag that we are running a dispose this allows running tasks and spin waits to short circuit + _disposing = true; + + _channel.Writer.TryComplete(); + + //Writing task will short circuit once _disposing is flipped to true + if (WritingTask != null) + await WritingTask.ConfigureAwait(false); + + if (_streamWriter != null) + await _streamWriter.DisposeAsync().ConfigureAwait(false); + } + +} diff --git a/src/Elastic.OpenTelemetry/Diagnostics/Logging/LogFormatter.cs b/src/Elastic.OpenTelemetry/Diagnostics/Logging/LogFormatter.cs new file mode 100644 index 0000000..a1f1bc7 --- /dev/null +++ b/src/Elastic.OpenTelemetry/Diagnostics/Logging/LogFormatter.cs @@ -0,0 +1,99 @@ +// 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; +using System.Text; +using Microsoft.Extensions.Logging; + +namespace Elastic.OpenTelemetry.Diagnostics.Logging; + +internal static class LogFormatter +{ + public static string Format(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) + { + var managedThreadId = Environment.CurrentManagedThreadId; + var dateTime = DateTime.UtcNow; + string? spanId = null; + var activity = Activity.Current; + switch (state) + { + case LogState s: + managedThreadId = s.ManagedThreadId; + dateTime = s.DateTime; + activity = s.Activity; + spanId = s.SpanId; + break; + } + + var builder = StringBuilderCache.Acquire(); + + WriteLogPrefix(managedThreadId, dateTime, logLevel, builder, spanId ?? activity?.SpanId.ToHexString() ?? string.Empty); + var message = formatter(state, exception); + builder.Append(message); + + //todo force Exception to be written as error + + if (activity is not null) + { + // Accessing activity.Id here will cause the Id to be initialized + // before the sampler runs in case where the activity is created using legacy way + // i.e. new Activity("Operation name"). This will result in Id not reflecting the + // correct sampling flags + // https://github.com/dotnet/runtime/issues/61857 + + var activityId = $"00-{activity.TraceId.ToHexString()}-{activity.SpanId.ToHexString()}-{(activity.ActivityTraceFlags.HasFlag(ActivityTraceFlags.Recorded) ? "01" : "00")}"; + builder.Append($" <{activityId}>"); + } + var fullLogLine = StringBuilderCache.GetStringAndRelease(builder); + return fullLogLine; + } + + private static void WriteLogPrefix(LogLevel logLevel, StringBuilder builder) => + WriteLogPrefix(Environment.CurrentManagedThreadId, DateTime.UtcNow, logLevel, builder); + + private const string EmptySpanId = "------"; + + private static void WriteLogPrefix(int managedThreadId, DateTime dateTime, LogLevel level, StringBuilder builder, string spanId = "") + { + const int maxLength = 5; + + if (string.IsNullOrEmpty(spanId)) + spanId = EmptySpanId; + + var threadId = new string('-', maxLength); + + if (managedThreadId > 0) + { + var digits = (int)Math.Floor(Math.Log10(managedThreadId) + 1); + + if (digits < 5) + { + Span buffer = stackalloc char[maxLength]; + for (var i = 0; i < maxLength - digits; i++) + buffer[i] = '0'; + managedThreadId.TryFormat(buffer[(maxLength - digits)..], out _); + threadId = buffer.ToString(); + } + else + threadId = managedThreadId.ToString(); + } + + builder.Append('[') + .Append(dateTime.ToString("yyyy-MM-dd HH:mm:ss.fff")) + .Append("][") + .Append(threadId) + .Append("][") + .Append(spanId[..6]) + .Append("][") + .Append(level.AsString()) + .Append(']'); + + var length = builder.Length; + var padding = 52 - length; + + for (var i = 0; i < padding; i++) + builder.Append(' '); + } + +} diff --git a/src/Elastic.OpenTelemetry/Diagnostics/DiagnosticErrorLevels.cs b/src/Elastic.OpenTelemetry/Diagnostics/Logging/LogLevelHelpers.cs similarity index 66% rename from src/Elastic.OpenTelemetry/Diagnostics/DiagnosticErrorLevels.cs rename to src/Elastic.OpenTelemetry/Diagnostics/Logging/LogLevelHelpers.cs index daf96e7..c009233 100644 --- a/src/Elastic.OpenTelemetry/Diagnostics/DiagnosticErrorLevels.cs +++ b/src/Elastic.OpenTelemetry/Diagnostics/Logging/LogLevelHelpers.cs @@ -1,15 +1,20 @@ // 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.Diagnostics; -internal static class DiagnosticErrorLevels +using Microsoft.Extensions.Logging; + +namespace Elastic.OpenTelemetry.Diagnostics.Logging; + +internal static class LogLevelHelpers { public const string Critical = "Critical"; public const string Error = "Error"; public const string Warning = "Warn"; public const string Info = "Info"; + public const string Information = "Information"; public const string Trace = "Trace"; + public const string Debug = "Debug"; public static LogLevel ToLogLevel(string logLevelString) => logLevelString switch @@ -17,9 +22,11 @@ public static LogLevel ToLogLevel(string logLevelString) => Critical => LogLevel.Critical, Error => LogLevel.Error, Warning => LogLevel.Warning, - Info => LogLevel.Info, + Info => LogLevel.Information, + Information => LogLevel.Information, + Debug => LogLevel.Debug, Trace => LogLevel.Trace, - _ => LogLevel.Unknown, + _ => LogLevel.None, }; public static string AsString(this LogLevel logLevel) => @@ -28,9 +35,10 @@ public static string AsString(this LogLevel logLevel) => LogLevel.Critical => Critical, LogLevel.Error => Error, LogLevel.Warning => Warning, - LogLevel.Info => Info, + LogLevel.Information => Info, + LogLevel.Debug => Debug, LogLevel.Trace => Trace, - LogLevel.Unknown => string.Empty, + LogLevel.None => string.Empty, _ => string.Empty }; } diff --git a/src/Elastic.OpenTelemetry/Diagnostics/Logging/ScopedCompositeLogger.cs b/src/Elastic.OpenTelemetry/Diagnostics/Logging/ScopedCompositeLogger.cs new file mode 100644 index 0000000..4359cc0 --- /dev/null +++ b/src/Elastic.OpenTelemetry/Diagnostics/Logging/ScopedCompositeLogger.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 Microsoft.Extensions.Logging; + +namespace Elastic.OpenTelemetry.Diagnostics.Logging; + +/// +/// A logger that writes to both and the user configured logger. +/// Instances of this never dispose so can be used anywhere even on short lived objects +/// +public sealed class ScopedCompositeLogger(ILogger? additionalLogger, ILogger fileLogger) : IDisposable, IAsyncDisposable, ILogger +{ + /// TODO + public void Dispose() + { + if (additionalLogger is IDisposable d) + d.Dispose(); + } + + /// TODO + public ValueTask DisposeAsync() => additionalLogger is IAsyncDisposable d ? d.DisposeAsync() : ValueTask.CompletedTask; + + /// TODO + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) + { + if (fileLogger.IsEnabled(logLevel)) + fileLogger.Log(logLevel, eventId, state, exception, formatter); + + if (additionalLogger == null) return; + + if (additionalLogger.IsEnabled(logLevel)) + additionalLogger.Log(logLevel, eventId, state, exception, formatter); + } + + /// TODO + public bool IsEnabled(LogLevel logLevel) => fileLogger.IsEnabled(logLevel) || (additionalLogger?.IsEnabled(logLevel) ?? false); + + /// TODO + public IDisposable BeginScope(TState state) where TState : notnull => + new CompositeDisposable(fileLogger.BeginScope(state), additionalLogger?.BeginScope(state)); + + private class CompositeDisposable(params IDisposable?[] disposables) : IDisposable + { + public void Dispose() + { + foreach(var disposable in disposables) + disposable?.Dispose(); + } + } +} diff --git a/src/Elastic.OpenTelemetry/Diagnostics/LoggingEventListener.cs b/src/Elastic.OpenTelemetry/Diagnostics/LoggingEventListener.cs index 4cd0ca5..e156683 100644 --- a/src/Elastic.OpenTelemetry/Diagnostics/LoggingEventListener.cs +++ b/src/Elastic.OpenTelemetry/Diagnostics/LoggingEventListener.cs @@ -4,6 +4,8 @@ using System.Diagnostics.Tracing; using System.Text; using System.Text.RegularExpressions; +using Elastic.OpenTelemetry.Diagnostics.Logging; +using Microsoft.Extensions.Logging; namespace Elastic.OpenTelemetry.Diagnostics; @@ -11,22 +13,22 @@ internal sealed partial class LoggingEventListener : EventListener, IAsyncDispos { public const string OpenTelemetrySdkEventSourceNamePrefix = "OpenTelemetry-"; - private readonly LogFileWriter _logFileWriter; + private readonly ILogger _logger; private readonly EventLevel _eventLevel = EventLevel.Informational; [GeneratedRegex("^\\d{2}-[a-f0-9]{32}-[a-f0-9]{16}-\\d{2}$")] private static partial Regex TraceParentRegex(); - public LoggingEventListener(LogFileWriter logFileWriter) + public LoggingEventListener(ILogger logger) { - _logFileWriter = logFileWriter; + _logger = logger; - var eventLevel = LogFileWriter.GetConfiguredLogLevel(); + var eventLevel = AgentLoggingHelpers.GetElasticOtelLogLevel(); _eventLevel = eventLevel switch { LogLevel.Trace => EventLevel.Verbose, - LogLevel.Info => EventLevel.Informational, + LogLevel.Information => EventLevel.Informational, LogLevel.Warning => EventLevel.Warning, LogLevel.Error => EventLevel.Error, LogLevel.Critical => EventLevel.Critical, @@ -36,11 +38,14 @@ public LoggingEventListener(LogFileWriter logFileWriter) public override void Dispose() { - _logFileWriter.Dispose(); + if (_logger is IDisposable d) + d.Dispose(); base.Dispose(); } - public ValueTask DisposeAsync() => _logFileWriter.DisposeAsync(); + public ValueTask DisposeAsync() => + _logger is IAsyncDisposable d ? d.DisposeAsync() : ValueTask.CompletedTask; + protected override void OnEventSourceCreated(EventSource eventSource) { @@ -71,7 +76,7 @@ protected override void OnEventWritten(EventWrittenEventArgs eventData) // TODO - We can only get the OS thread ID from the args - Do we send that instead?? // As per this issue - https://github.com/dotnet/runtime/issues/13125 - OnEventWritten may be on a different thread // so we can't use the Environment.CurrentManagedThreadId value here. - _logFileWriter.WriteLogLine(null, -1, eventData.TimeStamp, GetLogLevel(eventData), StringBuilderCache.GetStringAndRelease(builder), spanId); + _logger.WriteLogLine(null, -1, eventData.TimeStamp, GetLogLevel(eventData), StringBuilderCache.GetStringAndRelease(builder), spanId); } catch (Exception) { @@ -85,10 +90,10 @@ static LogLevel GetLogLevel(EventWrittenEventArgs eventData) => EventLevel.Critical => LogLevel.Critical, EventLevel.Error => LogLevel.Error, EventLevel.Warning => LogLevel.Warning, - EventLevel.Informational => LogLevel.Info, + EventLevel.Informational => LogLevel.Information, EventLevel.Verbose => LogLevel.Trace, - EventLevel.LogAlways => LogLevel.Unknown, - _ => LogLevel.Unknown + EventLevel.LogAlways => LogLevel.Information, + _ => LogLevel.None }; static string? CreateLogMessage(EventWrittenEventArgs eventData, StringBuilder builder) diff --git a/src/Elastic.OpenTelemetry/Diagnostics/Payloads/AddProcessorPayload.cs b/src/Elastic.OpenTelemetry/Diagnostics/Payloads/AddProcessorPayload.cs deleted file mode 100644 index b94761d..0000000 --- a/src/Elastic.OpenTelemetry/Diagnostics/Payloads/AddProcessorPayload.cs +++ /dev/null @@ -1,6 +0,0 @@ -// 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.Diagnostics; - -internal readonly record struct AddProcessorPayload(Type ProcessorType, Type BuilderType); diff --git a/src/Elastic.OpenTelemetry/Diagnostics/Payloads/AddSourcePayload.cs b/src/Elastic.OpenTelemetry/Diagnostics/Payloads/AddSourcePayload.cs deleted file mode 100644 index 6741a7d..0000000 --- a/src/Elastic.OpenTelemetry/Diagnostics/Payloads/AddSourcePayload.cs +++ /dev/null @@ -1,6 +0,0 @@ -// 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.Diagnostics; - -internal readonly record struct AddSourcePayload(string Name, Type BuilderType); diff --git a/src/Elastic.OpenTelemetry/EnvironmentVariables.cs b/src/Elastic.OpenTelemetry/EnvironmentVariables.cs index f5fc33c..f83d1a0 100644 --- a/src/Elastic.OpenTelemetry/EnvironmentVariables.cs +++ b/src/Elastic.OpenTelemetry/EnvironmentVariables.cs @@ -5,7 +5,6 @@ namespace Elastic.OpenTelemetry; internal static class EnvironmentVariables { - public const string ElasticOtelFileLogging = "ELASTIC_OTEL_ENABLE_FILE_LOGGING"; public const string ElasticOtelLogDirectoryEnvironmentVariable = "ELASTIC_OTEL_LOG_DIRECTORY"; public const string ElasticOtelLogLevelEnvironmentVariable = "ELASTIC_OTEL_LOG_LEVEL"; } diff --git a/src/Elastic.OpenTelemetry/Extensions/MeterProviderBuilderExtensions.cs b/src/Elastic.OpenTelemetry/Extensions/MeterProviderBuilderExtensions.cs deleted file mode 100644 index 848de8f..0000000 --- a/src/Elastic.OpenTelemetry/Extensions/MeterProviderBuilderExtensions.cs +++ /dev/null @@ -1,21 +0,0 @@ -// 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; -using OpenTelemetry.Metrics; - -using static Elastic.OpenTelemetry.Diagnostics.ElasticOpenTelemetryDiagnostics; - -namespace Elastic.OpenTelemetry.Extensions; - -/// -/// Provides Elastic APM extensions to . -/// -public static class MeterProviderBuilderExtensions -{ - internal static MeterProviderBuilder LogAndAddMeter(this MeterProviderBuilder builder, string meterName) - { - Log(MeterAddedEvent, () => new DiagnosticEvent(new(meterName, builder.GetType()))); - return builder.AddMeter(meterName); - } -} diff --git a/src/Elastic.OpenTelemetry/Extensions/TracerProviderBuilderExtensions.cs b/src/Elastic.OpenTelemetry/Extensions/TracerProviderBuilderExtensions.cs index 47576d8..4df944f 100644 --- a/src/Elastic.OpenTelemetry/Extensions/TracerProviderBuilderExtensions.cs +++ b/src/Elastic.OpenTelemetry/Extensions/TracerProviderBuilderExtensions.cs @@ -6,8 +6,7 @@ using OpenTelemetry; using System.Diagnostics; using Elastic.OpenTelemetry.Diagnostics; - -using static Elastic.OpenTelemetry.Diagnostics.ElasticOpenTelemetryDiagnostics; +using Microsoft.Extensions.Logging; namespace Elastic.OpenTelemetry.Extensions; @@ -16,22 +15,19 @@ namespace Elastic.OpenTelemetry.Extensions; /// public static class TracerProviderBuilderExtensions { - //TODO binder source generator on Build() to make it automatic? - /// - /// Include Elastic APM Trace Processors to ensure data is enriched and extended. - /// - public static TracerProviderBuilder AddElasticProcessors(this TracerProviderBuilder builder) => - builder.LogAndAddProcessor(new TransactionIdProcessor()); + /// Include Elastic APM Trace Processors to ensure data is enriched and extended. + public static TracerProviderBuilder AddElasticProcessors(this TracerProviderBuilder builder, ILogger logger) => + builder.LogAndAddProcessor(new TransactionIdProcessor(logger), logger); - internal static TracerProviderBuilder LogAndAddProcessor(this TracerProviderBuilder builder, BaseProcessor processor) + private static TracerProviderBuilder LogAndAddProcessor(this TracerProviderBuilder builder, BaseProcessor processor, ILogger logger) { - Log(ProcessorAddedEvent, () => new DiagnosticEvent(new(processor.GetType(), builder.GetType()))); + logger.LogProcessorAdded(processor.GetType().ToString(), builder.GetType().Name); return builder.AddProcessor(processor); } - internal static TracerProviderBuilder LogAndAddSource(this TracerProviderBuilder builder, string sourceName) + internal static TracerProviderBuilder LogAndAddSource(this TracerProviderBuilder builder, string sourceName, ILogger logger) { - Log(SourceAddedEvent, () => new DiagnosticEvent(new(sourceName, builder.GetType()))); + logger.LogSourceAdded(sourceName, builder.GetType().Name); return builder.AddSource(sourceName); } } diff --git a/src/Elastic.OpenTelemetry/Processors/TransactionIdProcessor.cs b/src/Elastic.OpenTelemetry/Processors/TransactionIdProcessor.cs index 448a820..958b76d 100644 --- a/src/Elastic.OpenTelemetry/Processors/TransactionIdProcessor.cs +++ b/src/Elastic.OpenTelemetry/Processors/TransactionIdProcessor.cs @@ -6,20 +6,17 @@ using Microsoft.Extensions.Logging; using OpenTelemetry; -using static Elastic.OpenTelemetry.Diagnostics.ElasticOpenTelemetryDiagnostics; - namespace Elastic.OpenTelemetry.Processors; /// /// /// -public class TransactionIdProcessor : BaseProcessor +public class TransactionIdProcessor(ILogger logger) : BaseProcessor { - private ILogger? _logger = null; private readonly AsyncLocal _currentTransactionId = new(); /// - /// + /// /// public const string TransactionIdTagName = "transaction.id"; @@ -29,10 +26,9 @@ public override void OnStart(Activity activity) if (activity.Parent == null) _currentTransactionId.Value = activity.SpanId; - if (_currentTransactionId.Value.HasValue) - { - activity.SetTag(TransactionIdTagName, _currentTransactionId.Value.Value.ToString()); - Log(TransactionIdAddedEvent, () => DiagnosticEvent.Create(ref _logger, activity)); - } + if (!_currentTransactionId.Value.HasValue) return; + + activity.SetTag(TransactionIdTagName, _currentTransactionId.Value.Value.ToString()); + logger.TransactionIdProcessorTagAdded(); } } diff --git a/tests/Elastic.OpenTelemetry.Tests/LoggingTests.cs b/tests/Elastic.OpenTelemetry.Tests/LoggingTests.cs new file mode 100644 index 0000000..166241b --- /dev/null +++ b/tests/Elastic.OpenTelemetry.Tests/LoggingTests.cs @@ -0,0 +1,75 @@ +// 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.ObjectModel; +using Elastic.OpenTelemetry.Diagnostics.Logging; +using Microsoft.Extensions.Logging; +using Xunit.Abstractions; + +namespace Elastic.OpenTelemetry.Tests; + +public class TestLogger(ITestOutputHelper testOutputHelper) : ILogger +{ + private readonly List _messages = new(); + public IReadOnlyCollection Messages => _messages.AsReadOnly(); + + public IDisposable BeginScope(TState state) where TState : notnull => NoopDisposable.Instance; + + public bool IsEnabled(LogLevel logLevel) + => true; + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) + { + var message = LogFormatter.Format(logLevel, eventId, state, exception, formatter); + _messages.Add(message); + testOutputHelper.WriteLine(message); + if (exception != null) + testOutputHelper.WriteLine(exception.ToString()); + } + + private class NoopDisposable : IDisposable + { + public static readonly NoopDisposable Instance = new(); + + public void Dispose() { } + } +} + +public class LoggingTests(ITestOutputHelper output) +{ + [Fact] + public async Task ObserveLogging() + { + var logger = new TestLogger(output); + const string activitySourceName = nameof(ObserveLogging); + + var activitySource = new ActivitySource(activitySourceName, "1.0.0"); + + await using (new AgentBuilder(logger) + .SkipOtlpExporter() + .ConfigureTracer(tpb => tpb + .ConfigureResource(rb => rb.AddService("Test", "1.0.0")) + .AddSource(activitySourceName) + .AddInMemoryExporter(new List())) + .Build()) + { + using (var activity = activitySource.StartActivity("DoingStuff", ActivityKind.Internal)) + { + activity?.SetStatus(ActivityStatusCode.Ok); + } + } + + //assert preamble information gets logged + logger.Messages.Should().ContainMatch("*Elastic OpenTelemetry Distribution:*"); + + var preambles = logger.Messages.Where(l => l.Contains("[Info] Elastic OpenTelemetry Distribution:")).ToList(); + preambles.Should().HaveCount(1); + + // assert agent initialized confirmation and stack trace gets dumped. + logger.Messages.Should().ContainMatch("*AgentBuilder initialized*"); + + // very lenient format check + logger.Messages.Should().ContainMatch("[*][*][*][Info]*"); + } +} diff --git a/tests/Elastic.OpenTelemetry.Tests/TransactionIdProcessorTests.cs b/tests/Elastic.OpenTelemetry.Tests/TransactionIdProcessorTests.cs index b96b96c..700a507 100644 --- a/tests/Elastic.OpenTelemetry.Tests/TransactionIdProcessorTests.cs +++ b/tests/Elastic.OpenTelemetry.Tests/TransactionIdProcessorTests.cs @@ -8,13 +8,14 @@ public class TransactionIdProcessorTests [Fact] public void TransactionId_IsAddedToTags() { - const string activitySourceName = "TestSource"; + const string activitySourceName = nameof(TransactionId_IsAddedToTags); var activitySource = new ActivitySource(activitySourceName, "1.0.0"); var exportedItems = new List(); using var agent = new AgentBuilder() + .SkipOtlpExporter() .ConfigureTracer(tpb => tpb .ConfigureResource(rb => rb.AddService("Test", "1.0.0")) .AddSource(activitySourceName)