class LoggingEventListener()

in src/Elastic.OpenTelemetry.Core/Diagnostics/LoggingEventListener.cs [21:149]


	class LoggingEventListener(ILogger logger, CompositeElasticOpenTelemetryOptions options) : EventListener, IAsyncDisposable
{
	public const string OpenTelemetrySdkEventSourceNamePrefix = "OpenTelemetry-";

	private readonly ILogger _logger = logger;
	private readonly EventLevel _eventLevel = options.EventLogLevel;

	private const string TraceParentRegularExpressionString = "^\\d{2}-[a-f0-9]{32}-[a-f0-9]{16}-\\d{2}$";
#if NET8_0_OR_GREATER
	[GeneratedRegex(TraceParentRegularExpressionString)]
	private static partial Regex TraceParentRegex();
#else
	private static readonly Regex TraceParentRegexExpression = new(TraceParentRegularExpressionString);
	private static Regex TraceParentRegex() => TraceParentRegexExpression;
#endif

	public override void Dispose()
	{
		if (_logger is IDisposable d)
			d.Dispose();

		base.Dispose();
	}

	public ValueTask DisposeAsync() =>
		_logger is IAsyncDisposable d ? d.DisposeAsync() : default;

	protected override void OnEventSourceCreated(EventSource eventSource)
	{
		if (eventSource.Name.StartsWith(OpenTelemetrySdkEventSourceNamePrefix, StringComparison.Ordinal))
			EnableEvents(eventSource, _eventLevel, EventKeywords.All);

		base.OnEventSourceCreated(eventSource);
	}

	protected override void OnEventWritten(EventWrittenEventArgs eventData)
	{
		if (!eventData.EventSource.Name.StartsWith(OpenTelemetrySdkEventSourceNamePrefix, StringComparison.Ordinal))
		{
			// Workaround for https://github.com/dotnet/runtime/issues/31927
			// EventCounters are published to all EventListeners, regardless of
			// which EventSource providers a listener is enabled for.
			return;
		}

		var logLevel = GetLogLevel(eventData);

		if (!_logger.IsEnabled(logLevel))
			return;

		// This should generally be reasonably efficient but we can consider switching
		// to a rented array and Span<char> if required.
		var builder = StringBuilderCache.Acquire();

#if NETSTANDARD2_0 || NETFRAMEWORK
		var timestamp = DateTime.UtcNow; //best effort in absence of real event timestamp
		var osThreadId = 0L;
#else
		var timestamp = eventData.TimeStamp;
		var osThreadId = eventData.OSThreadId;
#endif

		var spanId = CreateLogMessage(eventData, builder, osThreadId);

		// 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.
		_logger.WriteLogLine(null, -1, timestamp, logLevel, StringBuilderCache.GetStringAndRelease(builder), spanId);

		static LogLevel GetLogLevel(EventWrittenEventArgs eventData) =>
			eventData.Level switch
			{
				EventLevel.Critical => LogLevel.Critical,
				EventLevel.Error => LogLevel.Error,
				EventLevel.Warning => LogLevel.Warning,
				EventLevel.Informational => LogLevel.Information,
				EventLevel.Verbose => LogLevel.Trace,
				EventLevel.LogAlways => LogLevel.Information,
				_ => LogLevel.None
			};

		static string? CreateLogMessage(EventWrittenEventArgs eventData, StringBuilder builder, long threadId)
		{
			string? spanId = null;

			if (eventData.EventSource.Name.StartsWith(OpenTelemetrySdkEventSourceNamePrefix) && eventData.Message is not null)
			{
				builder.Append($"OTEL-SDK: [{threadId}] ");

				if (eventData.Payload is null)
				{
					builder.Append(eventData.Message);
					return spanId;
				}

				try
				{
					var matchedActivityId = eventData.Payload.SingleOrDefault(p => p is string ps && TraceParentRegex().IsMatch(ps));

					if (matchedActivityId is string payloadString)
						spanId = payloadString[36..^3];

					var message = string.Format(eventData.Message, [.. eventData.Payload]);
					builder.Append(message);
					return spanId;
				}
				catch
				{
					for (var i = 0; i < eventData.Payload.Count; i++)
					{
						builder.Append(" | ");

						var payload = eventData.Payload[i];

						if (payload is not null)
#if NETFRAMEWORK
							// ReSharper disable once NullCoalescingConditionIsAlwaysNotNullAccordingToAPIContract
#endif
							builder.Append(payload.ToString() ?? "null");
						else
							builder.Append("null");
					}
				}
			}

			return spanId;
		}
	}
}