Skip to content
Draft
52 changes: 49 additions & 3 deletions src/Exceptionless.Core/Configuration/MessageBusOptions.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
using Exceptionless.Core.Extensions;
using System.Diagnostics.CodeAnalysis;
using Exceptionless.Core.Extensions;
using Foundatio.Utility;
using Microsoft.Extensions.Configuration;

namespace Exceptionless.Core.Configuration;

public class MessageBusOptions
{
private const string RabbitMqProvider = "rabbitmq";
private const string ServerKey = "server";

public string? ConnectionString { get; internal set; }
public string? Provider { get; internal set; }
public Dictionary<string, string?> Data { get; internal set; } = null!;
Expand All @@ -24,12 +28,24 @@ public static MessageBusOptions ReadFromConfiguration(IConfiguration config, App

if (cs != null)
{
if (TryGetRabbitMqConnectionString(config, cs, out string? connectionString))
{
options.Provider = RabbitMqProvider;
options.ConnectionString = connectionString;
options.Data = new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase)
{
[nameof(options.Provider)] = options.Provider
};
options.Data[ServerKey] = connectionString;

return options;
}

options.Data = cs.ParseConnectionString();
options.Provider = options.Data.GetString(nameof(options.Provider));
string? providerConnectionString = !String.IsNullOrEmpty(options.Provider) ? config.GetConnectionString(options.Provider) : null;

var providerOptions = providerConnectionString.ParseConnectionString(defaultKey: "server");
options.Data ??= new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase);
var providerOptions = providerConnectionString.ParseConnectionString(defaultKey: ServerKey);
options.Data.AddRange(providerOptions);

options.ConnectionString = options.Data.BuildConnectionString(new HashSet<string> { nameof(options.Provider) });
Expand All @@ -47,4 +63,34 @@ public static MessageBusOptions ReadFromConfiguration(IConfiguration config, App

return options;
}

private static bool TryGetRabbitMqConnectionString(IConfiguration config, string selector, [NotNullWhen(true)] out string? connectionString)
{
connectionString = null;

int separatorIndex = selector.IndexOf(';');
string providerSelector = separatorIndex >= 0 ? selector[..separatorIndex] : selector;
var providerData = providerSelector.ParseConnectionString();

if (!String.Equals(providerData.GetString(nameof(Provider)), RabbitMqProvider, StringComparison.OrdinalIgnoreCase))
return false;

string? configuredConnectionString = separatorIndex >= 0 ? selector[(separatorIndex + 1)..] : null;
if (String.IsNullOrWhiteSpace(configuredConnectionString))
configuredConnectionString = config.GetConnectionString(RabbitMqProvider);

if (String.IsNullOrWhiteSpace(configuredConnectionString))
return false;

connectionString = TrimMatchingQuotes(configuredConnectionString.Trim());
return true;
}

private static string TrimMatchingQuotes(string value)
{
if (value.Length >= 2 && ((value[0] == '"' && value[^1] == '"') || (value[0] == '\'' && value[^1] == '\'')))
return value[1..^1];

return value;
}
}
4 changes: 4 additions & 0 deletions src/Exceptionless.Job/appsettings.Development.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ ConnectionStrings:
# Elasticsearch: server=https://elastic:elastic@localhost:9200
# Cache: provider=redis;
# MessageBus: provider=redis;
# RabbitMQ examples (choose inline or separate):
# MessageBus: 'provider=rabbitmq;amqp://localhost/%2F'
# MessageBus: 'provider=rabbitmq'
# rabbitmq: 'amqp://localhost/%2F'
# Queue: provider=redis;
# Storage: provider=folder;path=..\Exceptionless.Web\storage
Email: smtp://localhost:1025
Expand Down
4 changes: 4 additions & 0 deletions src/Exceptionless.Job/appsettings.Production.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ ConnectionStrings:
# Elasticsearch: server=http://localhost:9200
# Cache: provider=redis;server="localhost,abortConnect=false"
# MessageBus: provider=redis;server="localhost,abortConnect=false"
# RabbitMQ examples (choose inline or separate):
# MessageBus: 'provider=rabbitmq;amqp://localhost/%2F'
# MessageBus: 'provider=rabbitmq'
# rabbitmq: 'amqp://localhost/%2F'
# Queue: provider=redis;server="localhost,abortConnect=false"
# Storage: ''
# Email: 'smtps://user:password@domain.com:587'
Expand Down
4 changes: 4 additions & 0 deletions src/Exceptionless.Job/appsettings.Staging.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ ConnectionStrings:
# Elasticsearch: server=http://localhost:9200;replicas=0
# Cache: provider=redis;
# MessageBus: provider=redis;
# RabbitMQ examples (choose inline or separate):
# MessageBus: 'provider=rabbitmq;amqp://localhost/%2F'
# MessageBus: 'provider=rabbitmq'
# rabbitmq: 'amqp://localhost/%2F'
# Queue: provider=redis;
# Storage: provider=folder;path=.\storage=
OAuth: FacebookId=395178683904310;GitHubId=7ef1dd5bfbc4ccf7f5ef;GoogleId=809763155066-enkkdmt4ierc33q9cft9nf5d5c02h30q.apps.googleusercontent.com;MicrosoftId=000000004C137E8B;SlackId=34500115540.177239122322;
Expand Down
4 changes: 4 additions & 0 deletions src/Exceptionless.Web/appsettings.Development.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ ConnectionStrings:
# Elasticsearch: server=https://elastic:elastic@localhost:9200
# Cache: provider=redis;
# MessageBus: provider=redis;
# RabbitMQ examples (choose inline or separate):
# MessageBus: 'provider=rabbitmq;amqp://localhost/%2F'
# MessageBus: 'provider=rabbitmq'
# rabbitmq: 'amqp://localhost/%2F'
# Queue: provider=redis;
# Storage: provider=folder;path=.\storage
# LDAP: ''
Expand Down
4 changes: 4 additions & 0 deletions src/Exceptionless.Web/appsettings.Production.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ ConnectionStrings:
# Elasticsearch: server=http://localhost:9200
# Cache: provider=redis;server="localhost,abortConnect=false"
# MessageBus: provider=redis;server="localhost,abortConnect=false"
# RabbitMQ examples (choose inline or separate):
# MessageBus: 'provider=rabbitmq;amqp://localhost/%2F'
# MessageBus: 'provider=rabbitmq'
# rabbitmq: 'amqp://localhost/%2F'
# Queue: provider=redis;server="localhost,abortConnect=false"
# Storage: ''
# Email: 'smtps://user:password@domain.com:587'
Expand Down
4 changes: 4 additions & 0 deletions src/Exceptionless.Web/appsettings.Staging.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ ConnectionStrings:
# Elasticsearch: server=http://localhost:9200;replicas=0
# Cache: provider=redis;
# MessageBus: provider=redis;
# RabbitMQ examples (choose inline or separate):
# MessageBus: 'provider=rabbitmq;amqp://localhost/%2F'
# MessageBus: 'provider=rabbitmq'
# rabbitmq: 'amqp://localhost/%2F'
# Queue: provider=redis;
# Storage: provider=folder;path=.\storage=
OAuth: FacebookId=395178683904310;GitHubId=7ef1dd5bfbc4ccf7f5ef;GoogleId=809763155066-enkkdmt4ierc33q9cft9nf5d5c02h30q.apps.googleusercontent.com;MicrosoftId=000000004C137E8B;SlackId=34500115540.177239122322;
Expand Down
92 changes: 92 additions & 0 deletions tests/Exceptionless.Tests/Configuration/MessageBusOptionsTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
using Exceptionless.Core;
using Exceptionless.Core.Configuration;
using Microsoft.Extensions.Configuration;
using Xunit;

namespace Exceptionless.Tests.Configuration;

public class MessageBusOptionsTests
{
[Theory]
[InlineData("provider=rabbitmq;amqp://localhost/%2F", "amqp://localhost/%2F")]
[InlineData("provider=rabbitmq;\"amqp://localhost/%2F\"", "amqp://localhost/%2F")]
[InlineData("provider=rabbitmq;'amqp://localhost/%2F'", "amqp://localhost/%2F")]
[InlineData(
" PROVIDER = \"RABBITMQ\" ; 'amqps://user:p%40ss@rabbit.example.com:5671/team%2Fprod?heartbeat=30&connection_timeout=10000' ",
"amqps://user:p%40ss@rabbit.example.com:5671/team%2Fprod?heartbeat=30&connection_timeout=10000")]
public void ReadFromConfiguration_WithInlineRabbitMqUri_PreservesRawConnectionString(string configuredConnectionString, string expectedConnectionString)
{
var options = ReadOptions(new Dictionary<string, string?>
{
["ConnectionStrings:MessageBus"] = configuredConnectionString
});

Assert.Equal("rabbitmq", options.Provider);
Assert.Equal(expectedConnectionString, options.ConnectionString);
Assert.Equal(expectedConnectionString, options.Data["server"]);
}

[Theory]
[InlineData("provider=rabbitmq", "amqp://localhost/%2F", "amqp://localhost/%2F")]
[InlineData("provider=rabbitmq;", "'amqp://localhost/%2F'", "amqp://localhost/%2F")]
[InlineData(
"provider=RaBbItMq",
"\"amqps://user:p%40ss@rabbit.example.com:5671/team%2Fprod?heartbeat=30\"",
"amqps://user:p%40ss@rabbit.example.com:5671/team%2Fprod?heartbeat=30")]
public void ReadFromConfiguration_WithNamedRabbitMqUri_PreservesRawConnectionString(string selector, string configuredConnectionString, string expectedConnectionString)
{
var options = ReadOptions(new Dictionary<string, string?>
{
["ConnectionStrings:MessageBus"] = selector,
["ConnectionStrings:rabbitmq"] = configuredConnectionString
});

Assert.Equal("rabbitmq", options.Provider);
Assert.Equal(expectedConnectionString, options.ConnectionString);
Assert.Equal(expectedConnectionString, options.Data["server"]);
}

[Fact]
public void ReadFromConfiguration_WithRedisProviderSettings_MergesLegacyKeyValueData()
{
var options = ReadOptions(new Dictionary<string, string?>
{
["ConnectionStrings:MessageBus"] = "provider=redis;ssl=true",
["ConnectionStrings:redis"] = "server=localhost:6379;abortConnect=false"
});

Assert.Equal("redis", options.Provider);
Assert.Equal("ssl=true;server=localhost:6379;abortConnect=false", options.ConnectionString);
Assert.Equal("true", options.Data["ssl"]);
Assert.Equal("localhost:6379", options.Data["server"]);
Assert.Equal("false", options.Data["abortConnect"]);
}

[Fact]
public void ReadFromConfiguration_WithInlineRedisConnectionString_PreservesLegacyFormatting()
{
var options = ReadOptions(new Dictionary<string, string?>
{
["ConnectionStrings:MessageBus"] = "provider=redis;server=localhost:6379,abortConnect=false"
});

Assert.Equal("redis", options.Provider);
Assert.Equal("server=localhost:6379,abortConnect=false", options.ConnectionString);
Assert.Equal("localhost:6379,abortConnect=false", options.Data["server"]);
}

private static MessageBusOptions ReadOptions(Dictionary<string, string?> values)
{
var configuration = CreateConfiguration(values);
var appOptions = new AppOptions { AppScope = "production" };

return MessageBusOptions.ReadFromConfiguration(configuration, appOptions);
}

private static IConfiguration CreateConfiguration(Dictionary<string, string?> values)
{
return new ConfigurationBuilder()
.AddInMemoryCollection(values)
.Build();
}
}
4 changes: 4 additions & 0 deletions tests/Exceptionless.Tests/appsettings.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ ConnectionStrings:
# Elasticsearch: server=https://elastic:elastic@localhost:9200
# Cache: provider=redis;
# MessageBus: provider=redis;
# RabbitMQ examples (choose inline or separate):
# MessageBus: 'provider=rabbitmq;amqp://localhost/%2F'
# MessageBus: 'provider=rabbitmq'
# rabbitmq: 'amqp://localhost/%2F'
# Queue: provider=redis;
Storage: provider=folder;path=..\..\..\..\..\src\Exceptionless.Web\storage

Expand Down
Loading