|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Collections.Concurrent;
using Aspire.Components.Common.Tests;
using Aspire.Hosting.RabbitMQ;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using RabbitMQ.Client;
using RabbitMQ.Client.Logging;
using Testcontainers.RabbitMq;
using Xunit;
namespace Aspire.RabbitMQ.Client.Tests;
public class AspireRabbitMQLoggingTests
{
/// <summary>
/// Tests that the RabbitMQ client logs are forwarded to the M.E.Logging correctly in an end-to-end scenario.
///
/// The easiest way to ensure a log is written is to start the RabbitMQ container, establish the connection,
/// and then stop the container. This will cause the RabbitMQ client to log an error message.
/// </summary>
[Fact]
[RequiresDocker]
public async Task EndToEndLoggingTest()
{
await using var rabbitMqContainer = new RabbitMqBuilder()
.WithImage($"{ComponentTestConstants.AspireTestContainerRegistry}/{RabbitMQContainerImageTags.Image}:{RabbitMQContainerImageTags.Tag}")
.Build();
await rabbitMqContainer.StartAsync();
var builder = Host.CreateEmptyApplicationBuilder(null);
builder.Configuration.AddInMemoryCollection([
new KeyValuePair<string, string?>("ConnectionStrings:messaging", rabbitMqContainer.GetConnectionString())
]);
builder.AddRabbitMQClient("messaging");
var tsc = new TaskCompletionSource();
var logger = new TestLogger();
logger.LoggedMessage = () =>
{
// wait for at least 2 logs to be written
if (logger.Logs.Count >= 2)
{
tsc.SetResult();
}
};
builder.Services.AddSingleton<ILoggerProvider>(sp => new LoggerProvider(logger));
using var host = builder.Build();
using var connection = host.Services.GetRequiredService<IConnection>();
await rabbitMqContainer.StopAsync();
await rabbitMqContainer.DisposeAsync();
await tsc.Task.WaitAsync(TimeSpan.FromMinutes(1));
var logs = logger.Logs.ToArray();
Assert.True(logs.Length >= 2, "Should be at least 2 logs written.");
Assert.Contains(logs, l => l.Level == LogLevel.Information && l.Message == "Performing automatic recovery");
Assert.Contains(logs, l => l.Level == LogLevel.Error && l.Message == "Connection recovery exception.");
}
[Fact]
public void TestInfoAndWarn()
{
var builder = Host.CreateEmptyApplicationBuilder(null);
builder.Services.AddSingleton<RabbitMQEventSourceLogForwarder>();
var logger = new TestLogger();
builder.Services.AddSingleton<ILoggerProvider>(sp => new LoggerProvider(logger));
using var host = builder.Build();
host.Services.GetRequiredService<RabbitMQEventSourceLogForwarder>().Start();
var message = "This is an informational message.";
RabbitMqClientEventSource.Log.Info(message);
var logs = logger.Logs.ToArray();
Assert.Single(logs);
Assert.Equal(LogLevel.Information, logs[0].Level);
Assert.Equal(message, logs[0].Message);
var warningMessage = "This is a warning message.";
RabbitMqClientEventSource.Log.Warn(warningMessage);
logs = logger.Logs.ToArray();
Assert.Equal(2, logs.Length);
Assert.Equal(LogLevel.Warning, logs[1].Level);
Assert.Equal(warningMessage, logs[1].Message);
}
[Fact]
public void TestExceptionWithoutInnerException()
{
var builder = Host.CreateEmptyApplicationBuilder(null);
builder.Services.AddSingleton<RabbitMQEventSourceLogForwarder>();
var logger = new TestLogger();
builder.Services.AddSingleton<ILoggerProvider>(sp => new LoggerProvider(logger));
using var host = builder.Build();
host.Services.GetRequiredService<RabbitMQEventSourceLogForwarder>().Start();
var exceptionMessage = "Test exception";
Exception testException;
try
{
throw new InvalidOperationException(exceptionMessage);
}
catch (Exception ex)
{
testException = ex;
}
Assert.NotNull(testException);
var logMessage = "This is an error message.";
RabbitMqClientEventSource.Log.Error(logMessage, testException);
var logs = logger.Logs.ToArray();
Assert.Single(logs);
Assert.Equal(LogLevel.Error, logs[0].Level);
Assert.Equal(logMessage, logs[0].Message);
var errorEvent = Assert.IsAssignableFrom<IReadOnlyList<KeyValuePair<string, object?>>>(logs[0].State);
Assert.Equal(3, errorEvent.Count);
Assert.Equal("exception.type", errorEvent[0].Key);
Assert.Equal("System.InvalidOperationException", errorEvent[0].Value);
Assert.Equal("exception.message", errorEvent[1].Key);
Assert.Equal(exceptionMessage, errorEvent[1].Value);
Assert.Equal("exception.stacktrace", errorEvent[2].Key);
Assert.Contains("AspireRabbitMQLoggingTests.TestException", errorEvent[2].Value?.ToString());
}
[Fact]
public void TestExceptionWithInnerException()
{
var builder = Host.CreateEmptyApplicationBuilder(null);
builder.Services.AddSingleton<RabbitMQEventSourceLogForwarder>();
var logger = new TestLogger();
builder.Services.AddSingleton<ILoggerProvider>(sp => new LoggerProvider(logger));
using var host = builder.Build();
host.Services.GetRequiredService<RabbitMQEventSourceLogForwarder>().Start();
var exceptionMessage = "Test exception";
Exception testException;
InvalidOperationException innerException = new("Inner exception");
try
{
throw new InvalidOperationException(exceptionMessage, innerException);
}
catch (Exception ex)
{
testException = ex;
}
Assert.NotNull(testException);
var logMessage = "This is an error message.";
RabbitMqClientEventSource.Log.Error(logMessage, testException);
var logs = logger.Logs.ToArray();
Assert.Single(logs);
Assert.Equal(LogLevel.Error, logs[0].Level);
Assert.Equal(logMessage, logs[0].Message);
var errorEvent = Assert.IsAssignableFrom<IReadOnlyList<KeyValuePair<string, object?>>>(logs[0].State);
Assert.Equal(4, errorEvent.Count);
Assert.Equal("exception.type", errorEvent[0].Key);
Assert.Equal("System.InvalidOperationException", errorEvent[0].Value);
Assert.Equal("exception.message", errorEvent[1].Key);
Assert.Equal(exceptionMessage, errorEvent[1].Value);
Assert.Equal("exception.stacktrace", errorEvent[2].Key);
Assert.Contains("AspireRabbitMQLoggingTests.TestException", errorEvent[2].Value?.ToString());
Assert.Equal("exception.innerexception", errorEvent[3].Key);
Assert.Equal($"{innerException.GetType()}: {innerException.Message}", errorEvent[3].Value?.ToString());
}
private sealed class LoggerProvider(TestLogger logger) : ILoggerProvider
{
public ILogger CreateLogger(string categoryName) => logger;
public void Dispose() { }
}
private sealed class TestLogger : ILogger
{
public BlockingCollection<(LogLevel Level, string Message, object? State)> Logs { get; } = new();
public Action? LoggedMessage { get; set; }
public IDisposable? BeginScope<TState>(TState state) where TState : notnull =>
NullLogger.Instance.BeginScope(state);
public bool IsEnabled(LogLevel logLevel) => true;
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
{
Logs.Add((logLevel, formatter(state, exception), state));
LoggedMessage?.Invoke();
}
}
}
|