Skip to content

Commit 0282715

Browse files
authored
Implement RabbitMQ logging (dotnet#662)
* Implement RabbitMQ logging Add an EventSource listener to listen to the RabbitMQ event source which logs info, warn, and error messages. Forward these messages to an ILogger. Fix dotnet#564 * Stop coding like a dinosaur.
1 parent f9eb4b7 commit 0282715

File tree

4 files changed

+233
-2
lines changed

4 files changed

+233
-2
lines changed

src/Components/Aspire.RabbitMQ.Client/AspireRabbitMQExtensions.cs

+5-1
Original file line numberDiff line numberDiff line change
@@ -76,14 +76,16 @@ private static void AddRabbitMQ(
7676

7777
IConnectionFactory CreateConnectionFactory(IServiceProvider sp)
7878
{
79-
var connectionString = settings.ConnectionString;
79+
// ensure the log forwarder is initialized
80+
sp.GetRequiredService<RabbitMQEventSourceLogForwarder>().Start();
8081

8182
var factory = new ConnectionFactory();
8283

8384
var configurationOptionsSection = configSection.GetSection("ConnectionFactory");
8485
configurationOptionsSection.Bind(factory);
8586

8687
// the connection string from settings should win over the one from the ConnectionFactory section
88+
var connectionString = settings.ConnectionString;
8789
if (!string.IsNullOrEmpty(connectionString))
8890
{
8991
factory.Uri = new(connectionString);
@@ -105,6 +107,8 @@ IConnectionFactory CreateConnectionFactory(IServiceProvider sp)
105107
builder.Services.AddKeyedSingleton<IConnection>(serviceKey, (sp, key) => CreateConnection(sp.GetRequiredKeyedService<IConnectionFactory>(key), settings.MaxConnectRetryCount));
106108
}
107109

110+
builder.Services.AddSingleton<RabbitMQEventSourceLogForwarder>();
111+
108112
if (settings.Tracing)
109113
{
110114
// Note that RabbitMQ.Client v6.6 doesn't have built-in support for tracing. See https://github.com/rabbitmq/rabbitmq-dotnet-client/pull/1261

src/Components/Aspire.RabbitMQ.Client/ConfigurationSchema.json

+9
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,13 @@
11
{
2+
"definitions": {
3+
"logLevel": {
4+
"properties": {
5+
"RabbitMQ.Client": {
6+
"$ref": "#/definitions/logLevelThreshold"
7+
}
8+
}
9+
}
10+
},
211
"properties": {
312
"Aspire": {
413
"type": "object",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
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+
}

src/Components/Telemetry.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ Aspire.Npgsql.EntityFrameworkCore.PostgreSQL:
161161

162162
Aspire.RabbitMQ.Client:
163163
- Log categories:
164-
- TODO
164+
- "RabbitMQ.Client"
165165
- Activity source names:
166166
- "Aspire.RabbitMQ.Client"
167167
- Metric names:

0 commit comments

Comments
 (0)