diff --git a/src/Components/Aspire.RabbitMQ.Client/README.md b/src/Components/Aspire.RabbitMQ.Client/README.md index 42176b27cf7..b8026592215 100644 --- a/src/Components/Aspire.RabbitMQ.Client/README.md +++ b/src/Components/Aspire.RabbitMQ.Client/README.md @@ -18,7 +18,7 @@ dotnet add package Aspire.RabbitMQ.Client ## Usage example -In the _AppHost.cs_ file of your project, call the `AddRabbitMQClient` extension method to register an `IConnection` for use via the dependency injection container. The method takes a connection name parameter. +In the _Program.cs_ file of your project, call the `AddRabbitMQClient` extension method to register an `IConnection` for use via the dependency injection container. The method takes a connection name parameter. ```csharp builder.AddRabbitMQClient("messaging"); @@ -89,6 +89,22 @@ You can also setup the [ConnectionFactory](https://rabbitmq.github.io/rabbitmq-d builder.AddRabbitMQClient("messaging", configureConnectionFactory: factory => factory.ClientProvidedName = "MyApp"); ``` +## Observability + +### Tracing + +The Aspire RabbitMQ Client integration includes the following activity sources for OpenTelemetry tracing: + +- `Aspire.RabbitMQ.Client` - Tracks connection establishment and retry attempts. +- `RabbitMQ.Client.Publisher` - Tracks message publishing operations. +- `RabbitMQ.Client.Subscriber` - Tracks message consumption operations. + +Tracing is enabled by default. To disable it, set `DisableTracing` to `true` in the settings: + +```csharp +builder.AddRabbitMQClient("messaging", settings => settings.DisableTracing = true); +``` + ## AppHost extensions In your AppHost project, install the `Aspire.Hosting.RabbitMQ` library with [NuGet](https://www.nuget.org): diff --git a/tests/Aspire.RabbitMQ.Client.Tests/ConformanceTests.cs b/tests/Aspire.RabbitMQ.Client.Tests/ConformanceTests.cs index 1f60d365010..1a70421ca5e 100644 --- a/tests/Aspire.RabbitMQ.Client.Tests/ConformanceTests.cs +++ b/tests/Aspire.RabbitMQ.Client.Tests/ConformanceTests.cs @@ -1,11 +1,14 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Diagnostics; using Aspire.TestUtilities; using Aspire.Components.ConformanceTests; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using OpenTelemetry.Trace; using RabbitMQ.Client; using Xunit; @@ -13,11 +16,15 @@ namespace Aspire.RabbitMQ.Client.Tests; public class ConformanceTests : ConformanceTests, IClassFixture { - private readonly RabbitMQContainerFixture _containerFixture; + private readonly RabbitMQContainerFixture? _containerFixture; + private string ConnectionString { get; set; } - public ConformanceTests(RabbitMQContainerFixture containerFixture, ITestOutputHelper? output = null) : base(output) + public ConformanceTests(RabbitMQContainerFixture? containerFixture, ITestOutputHelper? output = null) : base(output) { _containerFixture = containerFixture; + ConnectionString = (_containerFixture is not null && RequiresFeatureAttribute.IsFeatureSupported(TestFeature.Docker)) + ? _containerFixture.GetConnectionString() + : "amqp://localhost:5672"; } protected override ServiceLifetime ServiceLifetime => ServiceLifetime.Singleton; @@ -31,7 +38,11 @@ public ConformanceTests(RabbitMQContainerFixture containerFixture, ITestOutputHe protected override string[] RequiredLogCategories => Array.Empty(); - protected override string ActivitySourceName => ""; +#if RABBITMQ_V6 + protected override string ActivitySourceName => "Aspire.RabbitMQ.Client"; +#else + protected override string ActivitySourceName => "RabbitMQ.Client.Publisher"; +#endif protected override string? ConfigurationSectionName => "Aspire:RabbitMQ:Client"; @@ -69,15 +80,9 @@ protected override (string json, string error)[] InvalidJsonToErrorMessage => ne }; protected override void PopulateConfiguration(ConfigurationManager configuration, string? key = null) - { - var connectionString = RequiresFeatureAttribute.IsFeatureSupported(TestFeature.Docker) ? - _containerFixture.GetConnectionString() : - "amqp://localhost:5672"; - - configuration.AddInMemoryCollection([ - new(CreateConfigKey("Aspire:RabbitMQ:Client", key, "ConnectionString"), connectionString) + => configuration.AddInMemoryCollection([ + new(CreateConfigKey("Aspire:RabbitMQ:Client", key, "ConnectionString"), ConnectionString) ]); - } protected override void RegisterComponent(HostApplicationBuilder builder, Action? configure = null, string? key = null) { @@ -132,4 +137,48 @@ protected override void SetupConnectionInformationIsDelayValidated() { Assert.Skip("RabbitMQ connects to localhost by default if the connection information isn't available."); } + +#if !RABBITMQ_V6 + [Fact] + [RequiresFeature(TestFeature.Docker)] + public void TracingEnablesTheRightActivitySource() + => RemoteInvokeWithLogging(static connectionStringToUse => + RunWithConnectionString(connectionStringToUse, static obj => obj.RunActivitySourceTest(key: null)), + ConnectionString, Output); + + [Fact] + [RequiresFeature(TestFeature.Docker)] + public void TracingEnablesTheRightActivitySource_Keyed() + => RemoteInvokeWithLogging(static connectionStringToUse => + RunWithConnectionString(connectionStringToUse, static obj => obj.RunActivitySourceTest(key: "key")), + ConnectionString, Output); + + private void RunActivitySourceTest(string? key) + { + HostApplicationBuilder builder = CreateHostBuilder(key: key); + builder.Logging.AddConsole(); + RegisterComponent(builder, options => SetTracing(options, true), key); + + List exportedActivities = []; + builder.Services.AddOpenTelemetry().WithTracing(traceBuilder => traceBuilder.AddInMemoryExporter(exportedActivities)); + + using IHost host = builder.Build(); + host.Start(); + + IConnection service = key is null + ? host.Services.GetRequiredService() + : host.Services.GetRequiredKeyedService(key); + + // Clear activities generated during connection establishment (from "Aspire.RabbitMQ.Client" source) + exportedActivities.Clear(); + + TriggerActivity(service); + + Assert.NotEmpty(exportedActivities); + Assert.Contains(exportedActivities, activity => activity.Source.Name == ActivitySourceName); + } + + private static void RunWithConnectionString(string connectionString, Action test) + => test(new ConformanceTests(null) { ConnectionString = connectionString }); +#endif }