|
| 1 | +// Licensed to the .NET Foundation under one or more agreements. |
| 2 | +// The .NET Foundation licenses this file to you under the MIT license. |
| 3 | + |
| 4 | +using System.Collections; |
| 5 | +using System.Diagnostics; |
| 6 | +using System.Diagnostics.Tracing; |
| 7 | +using Microsoft.Extensions.Logging; |
| 8 | + |
| 9 | +namespace Aspire.RabbitMQ.Client; |
| 10 | + |
| 11 | +internal sealed class RabbitMQEventSourceLogForwarder : IDisposable |
| 12 | +{ |
| 13 | + private static readonly Func<ErrorEventSourceEvent, Exception?, string> s_formatErrorEvent = FormatErrorEvent; |
| 14 | + private static readonly Func<EventSourceEvent, Exception?, string> s_formatEvent = FormatEvent; |
| 15 | + |
| 16 | + private readonly ILogger _logger; |
| 17 | + private RabbitMQEventSourceListener? _listener; |
| 18 | + |
| 19 | + public RabbitMQEventSourceLogForwarder(ILoggerFactory loggerFactory) |
| 20 | + { |
| 21 | + _logger = loggerFactory.CreateLogger("RabbitMQ.Client"); |
| 22 | + } |
| 23 | + |
| 24 | + /// <summary> |
| 25 | + /// Initiates the log forwarding from the RabbitMQ event sources to a provided <see cref="ILoggerFactory"/>, call <see cref="Dispose"/> to stop forwarding. |
| 26 | + /// </summary> |
| 27 | + public void Start() |
| 28 | + { |
| 29 | + _listener ??= new RabbitMQEventSourceListener(LogEvent, EventLevel.Verbose); |
| 30 | + } |
| 31 | + |
| 32 | + private void LogEvent(EventWrittenEventArgs eventData) |
| 33 | + { |
| 34 | + var level = MapLevel(eventData.Level); |
| 35 | + var eventId = new EventId(eventData.EventId, eventData.EventName); |
| 36 | + |
| 37 | + // Special case the Error event so the Exception Details are written correctly |
| 38 | + if (eventData.EventId == 3 && |
| 39 | + eventData.EventName == "Error" && |
| 40 | + eventData.PayloadNames?.Count == 2 && |
| 41 | + eventData.Payload?.Count == 2 && |
| 42 | + eventData.PayloadNames[0] == "message" && |
| 43 | + eventData.PayloadNames[1] == "ex") |
| 44 | + { |
| 45 | + _logger.Log(level, eventId, new ErrorEventSourceEvent(eventData), null, s_formatErrorEvent); |
| 46 | + } |
| 47 | + else |
| 48 | + { |
| 49 | + Debug.Assert( |
| 50 | + (eventData.EventId == 1 && eventData.EventName == "Info") || |
| 51 | + (eventData.EventId == 2 && eventData.EventName == "Warn")); |
| 52 | + |
| 53 | + _logger.Log(level, eventId, new EventSourceEvent(eventData), null, s_formatEvent); |
| 54 | + } |
| 55 | + } |
| 56 | + |
| 57 | + private static string FormatErrorEvent(ErrorEventSourceEvent eventSourceEvent, Exception? ex) => |
| 58 | + eventSourceEvent.EventData.Payload?[0]?.ToString() ?? "<empty>"; |
| 59 | + |
| 60 | + private static string FormatEvent(EventSourceEvent eventSourceEvent, Exception? ex) => |
| 61 | + eventSourceEvent.EventData.Payload?[0]?.ToString() ?? "<empty>"; |
| 62 | + |
| 63 | + public void Dispose() => _listener?.Dispose(); |
| 64 | + |
| 65 | + private static LogLevel MapLevel(EventLevel level) => level switch |
| 66 | + { |
| 67 | + EventLevel.Critical => LogLevel.Critical, |
| 68 | + EventLevel.Error => LogLevel.Error, |
| 69 | + EventLevel.Informational => LogLevel.Information, |
| 70 | + EventLevel.Verbose => LogLevel.Debug, |
| 71 | + EventLevel.Warning => LogLevel.Warning, |
| 72 | + EventLevel.LogAlways => LogLevel.Information, |
| 73 | + _ => throw new ArgumentOutOfRangeException(nameof(level), level, null), |
| 74 | + }; |
| 75 | + |
| 76 | + private readonly struct EventSourceEvent : IReadOnlyList<KeyValuePair<string, object?>> |
| 77 | + { |
| 78 | + public EventWrittenEventArgs EventData { get; } |
| 79 | + |
| 80 | + public EventSourceEvent(EventWrittenEventArgs eventData) |
| 81 | + { |
| 82 | + // only Info and Warn events are expected, which always have 'message' as the only payload |
| 83 | + Debug.Assert(eventData.PayloadNames?.Count == 1 && eventData.PayloadNames[0] == "message"); |
| 84 | + |
| 85 | + EventData = eventData; |
| 86 | + } |
| 87 | + |
| 88 | + public IEnumerator<KeyValuePair<string, object?>> GetEnumerator() |
| 89 | + { |
| 90 | + for (var i = 0; i < Count; i++) |
| 91 | + { |
| 92 | + yield return this[i]; |
| 93 | + } |
| 94 | + } |
| 95 | + |
| 96 | + IEnumerator IEnumerable.GetEnumerator() |
| 97 | + { |
| 98 | + return GetEnumerator(); |
| 99 | + } |
| 100 | + |
| 101 | + public int Count => EventData.PayloadNames?.Count ?? 0; |
| 102 | + |
| 103 | + public KeyValuePair<string, object?> this[int index] => new(EventData.PayloadNames![index], EventData.Payload![index]); |
| 104 | + } |
| 105 | + |
| 106 | + private readonly struct ErrorEventSourceEvent : IReadOnlyList<KeyValuePair<string, object?>> |
| 107 | + { |
| 108 | + public EventWrittenEventArgs EventData { get; } |
| 109 | + |
| 110 | + public ErrorEventSourceEvent(EventWrittenEventArgs eventData) |
| 111 | + { |
| 112 | + EventData = eventData; |
| 113 | + } |
| 114 | + |
| 115 | + public IEnumerator<KeyValuePair<string, object?>> GetEnumerator() |
| 116 | + { |
| 117 | + for (var i = 0; i < Count; i++) |
| 118 | + { |
| 119 | + yield return this[i]; |
| 120 | + } |
| 121 | + } |
| 122 | + |
| 123 | + IEnumerator IEnumerable.GetEnumerator() |
| 124 | + { |
| 125 | + return GetEnumerator(); |
| 126 | + } |
| 127 | + |
| 128 | + public int Count => 5; |
| 129 | + |
| 130 | + public KeyValuePair<string, object?> this[int index] |
| 131 | + { |
| 132 | + get |
| 133 | + { |
| 134 | + Debug.Assert(EventData.PayloadNames?.Count == 2 && EventData.Payload?.Count == 2); |
| 135 | + Debug.Assert(EventData.PayloadNames[0] == "message"); |
| 136 | + Debug.Assert(EventData.PayloadNames[1] == "ex"); |
| 137 | + |
| 138 | + ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(index, 5); |
| 139 | + |
| 140 | + return index switch |
| 141 | + { |
| 142 | + 0 => new(EventData.PayloadNames[0], EventData.Payload[0]), |
| 143 | + < 5 => GetExData(EventData, index), |
| 144 | + _ => throw new UnreachableException() |
| 145 | + }; |
| 146 | + |
| 147 | + static KeyValuePair<string, object?> GetExData(EventWrittenEventArgs eventData, int index) |
| 148 | + { |
| 149 | + Debug.Assert(index >= 1 && index <= 4); |
| 150 | + Debug.Assert(eventData.Payload?.Count == 2); |
| 151 | + var exData = eventData.Payload[1] as IDictionary<string, object?>; |
| 152 | + Debug.Assert(exData is not null && exData.Count == 4); |
| 153 | + |
| 154 | + return index switch |
| 155 | + { |
| 156 | + 1 => new("ex.Type", exData["Type"]), |
| 157 | + 2 => new("ex.Message", exData["Message"]), |
| 158 | + 3 => new("ex.StackTrace", exData["StackTrace"]), |
| 159 | + 4 => new("ex.InnerException", exData["InnerException"]), |
| 160 | + _ => throw new UnreachableException() |
| 161 | + }; |
| 162 | + } |
| 163 | + } |
| 164 | + } |
| 165 | + } |
| 166 | + |
| 167 | + /// <summary> |
| 168 | + /// Implementation of <see cref="EventListener"/> that listens to events produced by the RabbitMQ.Client library. |
| 169 | + /// </summary> |
| 170 | + private sealed class RabbitMQEventSourceListener : EventListener |
| 171 | + { |
| 172 | + private readonly List<EventSource> _eventSources = new List<EventSource>(); |
| 173 | + |
| 174 | + private readonly Action<EventWrittenEventArgs> _log; |
| 175 | + private readonly EventLevel _level; |
| 176 | + |
| 177 | + public RabbitMQEventSourceListener(Action<EventWrittenEventArgs> log, EventLevel level) |
| 178 | + { |
| 179 | + _log = log; |
| 180 | + _level = level; |
| 181 | + |
| 182 | + foreach (EventSource eventSource in _eventSources) |
| 183 | + { |
| 184 | + OnEventSourceCreated(eventSource); |
| 185 | + } |
| 186 | + |
| 187 | + _eventSources.Clear(); |
| 188 | + } |
| 189 | + |
| 190 | + protected sealed override void OnEventSourceCreated(EventSource eventSource) |
| 191 | + { |
| 192 | + base.OnEventSourceCreated(eventSource); |
| 193 | + |
| 194 | + if (_log == null) |
| 195 | + { |
| 196 | + _eventSources.Add(eventSource); |
| 197 | + } |
| 198 | + |
| 199 | + if (eventSource.Name == "rabbitmq-dotnet-client" || eventSource.Name == "rabbitmq-client") |
| 200 | + { |
| 201 | + EnableEvents(eventSource, _level); |
| 202 | + } |
| 203 | + } |
| 204 | + |
| 205 | + protected sealed override void OnEventWritten(EventWrittenEventArgs eventData) |
| 206 | + { |
| 207 | + // Workaround https://github.com/dotnet/corefx/issues/42600 |
| 208 | + if (eventData.EventId == -1) |
| 209 | + { |
| 210 | + return; |
| 211 | + } |
| 212 | + |
| 213 | + // There is a very tight race during the listener creation where EnableEvents was called |
| 214 | + // and the thread producing events not observing the `_log` field assignment |
| 215 | + _log?.Invoke(eventData); |
| 216 | + } |
| 217 | + } |
| 218 | +} |
0 commit comments