diff --git a/Directory.Packages.props b/Directory.Packages.props index b234a984b..3896476ad 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -28,7 +28,7 @@ - + diff --git a/src/Http/Wolverine.Http/Transport/HttpEndpoint.cs b/src/Http/Wolverine.Http/Transport/HttpEndpoint.cs index 0e8a72985..74f420a1b 100644 --- a/src/Http/Wolverine.Http/Transport/HttpEndpoint.cs +++ b/src/Http/Wolverine.Http/Transport/HttpEndpoint.cs @@ -1,4 +1,5 @@ using System.Text.Json; +using JasperFx.Descriptors; using Microsoft.Extensions.Logging; using Wolverine.Configuration; using Wolverine.Runtime; @@ -21,6 +22,7 @@ public override ValueTask BuildListenerAsync(IWolverineRuntime runtim return ValueTask.FromResult(new NulloListener(Uri)); } + [IgnoreDescription] public JsonSerializerOptions SerializerOptions { get; set; } = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, diff --git a/src/Transports/AWS/Wolverine.AmazonSns/Internal/AmazonSnsSubscription.cs b/src/Transports/AWS/Wolverine.AmazonSns/Internal/AmazonSnsSubscription.cs index 02405fc52..6f37b3209 100644 --- a/src/Transports/AWS/Wolverine.AmazonSns/Internal/AmazonSnsSubscription.cs +++ b/src/Transports/AWS/Wolverine.AmazonSns/Internal/AmazonSnsSubscription.cs @@ -37,4 +37,10 @@ internal AmazonSnsSubscription(Subscription subscription) }; public AmazonSnsSubscriptionAttributes Attributes { get; } + + /// + /// Used by OptionsDescription (via [DescribeAsStringArray]) to render this + /// subscription as a single readable line. + /// + public override string ToString() => $"{Protocol}:{Endpoint}"; } diff --git a/src/Transports/AWS/Wolverine.AmazonSns/Internal/AmazonSnsTopic.cs b/src/Transports/AWS/Wolverine.AmazonSns/Internal/AmazonSnsTopic.cs index b6a39e106..a8e7946cb 100644 --- a/src/Transports/AWS/Wolverine.AmazonSns/Internal/AmazonSnsTopic.cs +++ b/src/Transports/AWS/Wolverine.AmazonSns/Internal/AmazonSnsTopic.cs @@ -3,6 +3,7 @@ using Amazon.SQS; using Amazon.SQS.Model; using JasperFx.Core; +using JasperFx.Descriptors; using Microsoft.Extensions.Logging; using Wolverine.Configuration; using Wolverine.Runtime; @@ -59,7 +60,9 @@ internal ISnsEnvelopeMapper BuildMapper(IWolverineRuntime runtime) public string TopicName { get; } public string TopicArn { get; set; } + [ChildDescription] public CreateTopicRequest Configuration { get; } + [DescribeAsStringArray] public IList TopicSubscriptions { get; set; } = new List(); public async ValueTask CheckAsync() diff --git a/src/Transports/AWS/Wolverine.AmazonSns/Internal/AmazonSnsTransport.cs b/src/Transports/AWS/Wolverine.AmazonSns/Internal/AmazonSnsTransport.cs index 7a5947b44..6df2e5a5b 100644 --- a/src/Transports/AWS/Wolverine.AmazonSns/Internal/AmazonSnsTransport.cs +++ b/src/Transports/AWS/Wolverine.AmazonSns/Internal/AmazonSnsTransport.cs @@ -2,6 +2,7 @@ using Amazon.SimpleNotificationService; using Amazon.SQS; using JasperFx.Core; +using JasperFx.Descriptors; using Wolverine.AmazonSqs.Internal; using Wolverine.Runtime; using Wolverine.Transports; @@ -29,9 +30,12 @@ internal AmazonSnsTransport(IAmazonSimpleNotificationService snsClient, IAmazonS public override Uri ResourceUri => new(SnsConfig.ServiceURL); + [DescribeAsConfigurationState] public Func? CredentialSource { get; set; } + [IgnoreDescription] public LightweightCache Topics { get; } - + + [ChildDescription] public AmazonSimpleNotificationServiceConfig SnsConfig { get; } = new(); internal IAmazonSimpleNotificationService? SnsClient { get; private set; } internal IAmazonSQS? SqsClient { get; private set; } @@ -131,6 +135,7 @@ private AmazonSQSClient buildSqsClient(IWolverineRuntime runtime) /// Override to customize the queue policy for permissions for an AWS SQS queue that subscribes to /// an SNS topic /// + [IgnoreDescription] public Func QueuePolicyBuilder { get; set; } = description => { var queuePolicy = $$""" diff --git a/src/Transports/AWS/Wolverine.AmazonSns/Internal/CloudEventsSnsMapper.cs b/src/Transports/AWS/Wolverine.AmazonSns/Internal/CloudEventsSnsMapper.cs index 5772c6285..2d747a7d1 100644 --- a/src/Transports/AWS/Wolverine.AmazonSns/Internal/CloudEventsSnsMapper.cs +++ b/src/Transports/AWS/Wolverine.AmazonSns/Internal/CloudEventsSnsMapper.cs @@ -12,6 +12,8 @@ public CloudEventsSnsMapper(CloudEventsMapper inner) _inner = inner; } + public override string ToString() => "Cloud Events"; + public string BuildMessageBody(Envelope envelope) { return _inner.WriteToString(envelope); diff --git a/src/Transports/AWS/Wolverine.AmazonSns/Internal/MassTransitMapper.cs b/src/Transports/AWS/Wolverine.AmazonSns/Internal/MassTransitMapper.cs index a5e1f985c..a9c4ba27f 100644 --- a/src/Transports/AWS/Wolverine.AmazonSns/Internal/MassTransitMapper.cs +++ b/src/Transports/AWS/Wolverine.AmazonSns/Internal/MassTransitMapper.cs @@ -16,6 +16,8 @@ public MassTransitMapper(IMassTransitInteropEndpoint endpoint) _serializer = new MassTransitJsonSerializer(endpoint); } + public override string ToString() => "MassTransit Interop"; + public MassTransitJsonSerializer Serializer => _serializer; public string BuildMessageBody(Envelope envelope) diff --git a/src/Transports/AWS/Wolverine.AmazonSns/Internal/NServiceBusEnvelopeMapper.cs b/src/Transports/AWS/Wolverine.AmazonSns/Internal/NServiceBusEnvelopeMapper.cs index b2c3e216a..5e7f0b6f6 100644 --- a/src/Transports/AWS/Wolverine.AmazonSns/Internal/NServiceBusEnvelopeMapper.cs +++ b/src/Transports/AWS/Wolverine.AmazonSns/Internal/NServiceBusEnvelopeMapper.cs @@ -22,6 +22,8 @@ public NServiceBusEnvelopeMapper(string replyName, Endpoint endpoint) _endpoint = endpoint; } + public override string ToString() => "NServiceBus Interop"; + public IEnumerable> ToAttributes(Envelope envelope) { yield return new ("NServiceBus.ConversationId", diff --git a/src/Transports/AWS/Wolverine.AmazonSns/RawJsonSnsEnvelopeMapper.cs b/src/Transports/AWS/Wolverine.AmazonSns/RawJsonSnsEnvelopeMapper.cs index cf05ef048..ac9dd9e8f 100644 --- a/src/Transports/AWS/Wolverine.AmazonSns/RawJsonSnsEnvelopeMapper.cs +++ b/src/Transports/AWS/Wolverine.AmazonSns/RawJsonSnsEnvelopeMapper.cs @@ -16,6 +16,8 @@ public RawJsonSnsEnvelopeMapper(Type defaultMessageType, JsonSerializerOptions s _serializerOptions = serializerOptions; } + public override string ToString() => "Raw JSON"; + public string BuildMessageBody(Envelope envelope) { return JsonSerializer.Serialize( diff --git a/src/Transports/AWS/Wolverine.AmazonSqs/Internal/AmazonSqsQueue.cs b/src/Transports/AWS/Wolverine.AmazonSqs/Internal/AmazonSqsQueue.cs index 7142f6bda..2ba62099e 100644 --- a/src/Transports/AWS/Wolverine.AmazonSqs/Internal/AmazonSqsQueue.cs +++ b/src/Transports/AWS/Wolverine.AmazonSqs/Internal/AmazonSqsQueue.cs @@ -2,6 +2,7 @@ using Amazon.SQS.Model; using JasperFx.Core; using JasperFx.Core.Reflection; +using JasperFx.Descriptors; using Microsoft.Extensions.Logging; using Wolverine.Configuration; using Wolverine.Runtime; @@ -84,6 +85,7 @@ public int VisibilityTimeout /// /// Additional configuration for how an SQS queue should be created /// + [ChildDescription] public CreateQueueRequest Configuration { get; } /// diff --git a/src/Transports/AWS/Wolverine.AmazonSqs/Internal/AmazonSqsTransport.cs b/src/Transports/AWS/Wolverine.AmazonSqs/Internal/AmazonSqsTransport.cs index 01861cbd4..b5903a119 100644 --- a/src/Transports/AWS/Wolverine.AmazonSqs/Internal/AmazonSqsTransport.cs +++ b/src/Transports/AWS/Wolverine.AmazonSqs/Internal/AmazonSqsTransport.cs @@ -2,6 +2,7 @@ using Amazon.SQS; using Amazon.SQS.Model; using JasperFx.Core; +using JasperFx.Descriptors; using Microsoft.Extensions.Logging; using Spectre.Console; using Wolverine.Configuration; @@ -41,10 +42,13 @@ internal AmazonSqsTransport(IAmazonSQS client) : this() Client = client; } + [DescribeAsConfigurationState] public Func? CredentialSource { get; set; } + [IgnoreDescription] public LightweightCache Queues { get; } + [ChildDescription] public AmazonSQSConfig Config { get; } = new(); internal IAmazonSQS? Client { get; private set; } diff --git a/src/Transports/AWS/Wolverine.AmazonSqs/Internal/CloudEventsSqsMapper.cs b/src/Transports/AWS/Wolverine.AmazonSqs/Internal/CloudEventsSqsMapper.cs index fa87014fc..079e60a37 100644 --- a/src/Transports/AWS/Wolverine.AmazonSqs/Internal/CloudEventsSqsMapper.cs +++ b/src/Transports/AWS/Wolverine.AmazonSqs/Internal/CloudEventsSqsMapper.cs @@ -13,6 +13,8 @@ public CloudEventsSqsMapper(CloudEventsMapper inner) _inner = inner; } + public override string ToString() => "Cloud Events"; + public string BuildMessageBody(Envelope envelope) { return _inner.WriteToString(envelope); diff --git a/src/Transports/AWS/Wolverine.AmazonSqs/Internal/MassTransitMapper.cs b/src/Transports/AWS/Wolverine.AmazonSqs/Internal/MassTransitMapper.cs index a42755561..e0ea531b7 100644 --- a/src/Transports/AWS/Wolverine.AmazonSqs/Internal/MassTransitMapper.cs +++ b/src/Transports/AWS/Wolverine.AmazonSqs/Internal/MassTransitMapper.cs @@ -18,6 +18,8 @@ public MassTransitMapper(IMassTransitInteropEndpoint endpoint) _serializer = new MassTransitJsonSerializer(endpoint); } + public override string ToString() => "MassTransit Interop"; + public MassTransitJsonSerializer Serializer => _serializer; public string BuildMessageBody(Envelope envelope) diff --git a/src/Transports/AWS/Wolverine.AmazonSqs/RawJsonSqsEnvelopeMapper.cs b/src/Transports/AWS/Wolverine.AmazonSqs/RawJsonSqsEnvelopeMapper.cs index d71b93cfe..0cf17b304 100644 --- a/src/Transports/AWS/Wolverine.AmazonSqs/RawJsonSqsEnvelopeMapper.cs +++ b/src/Transports/AWS/Wolverine.AmazonSqs/RawJsonSqsEnvelopeMapper.cs @@ -16,6 +16,8 @@ public RawJsonSqsEnvelopeMapper(Type defaultMessageType, JsonSerializerOptions s _serializerOptions = serializerOptions; } + public override string ToString() => "Raw JSON"; + public string BuildMessageBody(Envelope envelope) { return JsonSerializer.Serialize( diff --git a/src/Transports/Azure/Wolverine.AzureServiceBus/AzureServiceBusTransport.cs b/src/Transports/Azure/Wolverine.AzureServiceBus/AzureServiceBusTransport.cs index def0db8fd..b730d1337 100644 --- a/src/Transports/Azure/Wolverine.AzureServiceBus/AzureServiceBusTransport.cs +++ b/src/Transports/Azure/Wolverine.AzureServiceBus/AzureServiceBusTransport.cs @@ -3,6 +3,7 @@ using Azure.Messaging.ServiceBus; using Azure.Messaging.ServiceBus.Administration; using JasperFx.Core; +using JasperFx.Descriptors; using Microsoft.Extensions.Logging; using Wolverine.AzureServiceBus.Internal; using Wolverine.Configuration; @@ -84,9 +85,14 @@ public override string SanitizeIdentifier(string identifier) /// public bool SystemQueuesEnabled { get; set; } = true; + [IgnoreDescription] public LightweightCache Queues { get; } + [IgnoreDescription] public LightweightCache Topics { get; } + // Contains shared access key / password — raw value hidden to avoid + // leaking secrets in diagnostic views. Surfaced via ConnectionSummary below. + [IgnoreDescription] public string? ConnectionString { get; set; } /// @@ -94,13 +100,34 @@ public override string SanitizeIdentifier(string identifier) /// When set, the management client uses this instead of ConnectionString. /// Useful for the Azure Service Bus Emulator which exposes management on a different port. /// + [IgnoreDescription] public string? ManagementConnectionString { get; set; } + /// + /// The configured connection string with the SharedAccessKey value + /// masked. Safe to render in diagnostic output. + /// + public string? ConnectionSummary => ConnectionString == null + ? null + : ConnectionStringRedactor.Redact(ConnectionString, "SharedAccessKey"); + + /// + /// The configured management connection string with the SharedAccessKey + /// value masked. Safe to render in diagnostic output. + /// + public string? ManagementConnectionSummary => ManagementConnectionString == null + ? null + : ConnectionStringRedactor.Redact(ManagementConnectionString, "SharedAccessKey"); + public string? FullyQualifiedNamespace { get; set; } + [IgnoreDescription] public TokenCredential? TokenCredential { get; set; } + [IgnoreDescription] public AzureNamedKeyCredential? NamedKeyCredential { get; set; } + [IgnoreDescription] public AzureSasCredential? SasCredential { get; set; } + [ChildDescription] public ServiceBusClientOptions ClientOptions { get; } = new() { TransportType = ServiceBusTransportType.AmqpTcp diff --git a/src/Transports/Azure/Wolverine.AzureServiceBus/Internal/AzureServiceBusEndpoint.cs b/src/Transports/Azure/Wolverine.AzureServiceBus/Internal/AzureServiceBusEndpoint.cs index 1816d0791..01ad6cbf6 100644 --- a/src/Transports/Azure/Wolverine.AzureServiceBus/Internal/AzureServiceBusEndpoint.cs +++ b/src/Transports/Azure/Wolverine.AzureServiceBus/Internal/AzureServiceBusEndpoint.cs @@ -1,5 +1,6 @@ using Azure.Messaging.ServiceBus; using JasperFx.Core; +using JasperFx.Descriptors; using Microsoft.Extensions.Logging; using Wolverine.Configuration; using Wolverine.Runtime; @@ -31,6 +32,7 @@ public AzureServiceBusEndpoint(AzureServiceBusTransport parent, Uri uri, Endpoin Parent = parent; } + [IgnoreDescription] public AzureServiceBusTransport Parent { get; } /// diff --git a/src/Transports/Azure/Wolverine.AzureServiceBus/Internal/AzureServiceBusQueue.cs b/src/Transports/Azure/Wolverine.AzureServiceBus/Internal/AzureServiceBusQueue.cs index f19235c52..f80e6cbf8 100644 --- a/src/Transports/Azure/Wolverine.AzureServiceBus/Internal/AzureServiceBusQueue.cs +++ b/src/Transports/Azure/Wolverine.AzureServiceBus/Internal/AzureServiceBusQueue.cs @@ -4,6 +4,7 @@ using Azure.Messaging.ServiceBus.Administration; using JasperFx.Core; using JasperFx.Core.Reflection; +using JasperFx.Descriptors; using Microsoft.Extensions.Logging; using Newtonsoft.Json; using Wolverine.Configuration; @@ -36,6 +37,7 @@ public AzureServiceBusQueue(AzureServiceBusTransport parent, string queueName, }; } + [ChildDescription] public CreateQueueOptions Options { get; } public string QueueName { get; } diff --git a/src/Transports/Azure/Wolverine.AzureServiceBus/Internal/AzureServiceBusSubscription.cs b/src/Transports/Azure/Wolverine.AzureServiceBus/Internal/AzureServiceBusSubscription.cs index 4d3d4f4b5..e95bbad49 100644 --- a/src/Transports/Azure/Wolverine.AzureServiceBus/Internal/AzureServiceBusSubscription.cs +++ b/src/Transports/Azure/Wolverine.AzureServiceBus/Internal/AzureServiceBusSubscription.cs @@ -4,6 +4,7 @@ using Azure.Messaging.ServiceBus.Administration; using JasperFx.Core; using JasperFx.Core.Reflection; +using JasperFx.Descriptors; using Microsoft.Extensions.Logging; using Newtonsoft.Json; using Wolverine.Configuration; @@ -41,12 +42,16 @@ public AzureServiceBusSubscription(AzureServiceBusTransport parent, AzureService RuleOptions = new CreateRuleOptions(); } + [ChildDescription] public CreateSubscriptionOptions Options { get; } + [ChildDescription] public CreateRuleOptions RuleOptions { get; } public string SubscriptionName { get; } + // No attribute needed — AzureServiceBusTopic.ToString() returns TopicName, + // so this property renders as the topic name string (per audit decision). public AzureServiceBusTopic Topic { get; } public override ValueTask BuildListenerAsync(IWolverineRuntime runtime, IReceiver receiver) diff --git a/src/Transports/Azure/Wolverine.AzureServiceBus/Internal/AzureServiceBusTopic.cs b/src/Transports/Azure/Wolverine.AzureServiceBus/Internal/AzureServiceBusTopic.cs index 5662c3788..85059448c 100644 --- a/src/Transports/Azure/Wolverine.AzureServiceBus/Internal/AzureServiceBusTopic.cs +++ b/src/Transports/Azure/Wolverine.AzureServiceBus/Internal/AzureServiceBusTopic.cs @@ -3,6 +3,7 @@ using Azure.Messaging.ServiceBus.Administration; using JasperFx.Core; using JasperFx.Core.Reflection; +using JasperFx.Descriptors; using Microsoft.Extensions.Logging; using Newtonsoft.Json; using Wolverine.Configuration; @@ -33,6 +34,12 @@ public AzureServiceBusTopic(AzureServiceBusTransport parent, string topicName) : public string TopicName { get; } + /// + /// Used by OptionsDescription to render references to this topic (for example + /// from ) as just the topic name. + /// + public override string ToString() => TopicName; + public override ValueTask BuildListenerAsync(IWolverineRuntime runtime, IReceiver receiver) { throw new NotSupportedException(); @@ -60,6 +67,7 @@ public override ValueTask TeardownAsync(ILogger logger) return new ValueTask(Parent.WithManagementClientAsync(client => client.DeleteTopicAsync(TopicName))); } + [ChildDescription] public CreateTopicOptions Options { get; } public override ValueTask SetupAsync(ILogger logger) diff --git a/src/Transports/Kafka/Wolverine.Kafka/Internals/KafkaTransport.cs b/src/Transports/Kafka/Wolverine.Kafka/Internals/KafkaTransport.cs index e3e5aea7e..54babe66b 100644 --- a/src/Transports/Kafka/Wolverine.Kafka/Internals/KafkaTransport.cs +++ b/src/Transports/Kafka/Wolverine.Kafka/Internals/KafkaTransport.cs @@ -1,5 +1,6 @@ using Confluent.Kafka; using JasperFx.Core; +using JasperFx.Descriptors; using Wolverine.Configuration; using Wolverine.Runtime; using Wolverine.Runtime.Routing; @@ -16,17 +17,24 @@ public enum KafkaUsage public class KafkaTransport : BrokerTransport { + [IgnoreDescription] public Cache Topics { get; } internal List TopicGroups { get; } = new(); + [ChildDescription] public ProducerConfig ProducerConfig { get; } = new(); + [IgnoreDescription] public Action> ConfigureProducerBuilders { get; internal set; } = _ => {}; + [ChildDescription] public ConsumerConfig ConsumerConfig { get; } = new(); + [IgnoreDescription] public Action> ConfigureConsumerBuilders { get; internal set; } = _ => {}; + [ChildDescription] public AdminClientConfig AdminClientConfig { get; } = new(); + [IgnoreDescription] public Action ConfigureAdminClientBuilders { get; internal set; } = _ => {}; public KafkaTransport() : this("kafka") diff --git a/src/Transports/Kafka/Wolverine.Kafka/KafkaTopic.cs b/src/Transports/Kafka/Wolverine.Kafka/KafkaTopic.cs index b35eda901..928e60645 100644 --- a/src/Transports/Kafka/Wolverine.Kafka/KafkaTopic.cs +++ b/src/Transports/Kafka/Wolverine.Kafka/KafkaTopic.cs @@ -1,5 +1,6 @@ using Confluent.Kafka; using Confluent.Kafka.Admin; +using JasperFx.Descriptors; using Microsoft.Extensions.Logging; using System.Text; using Wolverine.Configuration; @@ -15,6 +16,7 @@ public class KafkaTopic : Endpoint, I // Strictly an identifier for the endpoint public const string WolverineTopicsName = "wolverine.topics"; + [IgnoreDescription] public KafkaTransport Parent { get; } public KafkaTopic(KafkaTransport parent, string topicName, EndpointRole role) : base(new Uri($"{parent.Protocol}://topic/" + topicName), role) @@ -36,6 +38,7 @@ public override bool AutoStartSendingAgent() return true; } + [ChildDescription] public TopicSpecification Specification { get; } = new(); public string TopicName { get; } @@ -43,11 +46,13 @@ public override bool AutoStartSendingAgent() /// /// Override for this specific Kafka Topic /// + [ChildDescription] public ConsumerConfig? ConsumerConfig { get; internal set; } /// /// Override for this specific Kafka Topic /// + [ChildDescription] public ProducerConfig? ProducerConfig { get; internal set; } /// @@ -204,6 +209,7 @@ public override async ValueTask InitializeAsync(ILogger logger) /// /// Override how this Kafka topic is created /// + [IgnoreDescription] public Func CreateTopicFunc { get; internal set; } = (c, t) => c.CreateTopicsAsync([t.Specification]); } diff --git a/src/Transports/MQTT/Wolverine.MQTT/Internals/MqttTransport.cs b/src/Transports/MQTT/Wolverine.MQTT/Internals/MqttTransport.cs index fc07c20d2..ded7eb063 100644 --- a/src/Transports/MQTT/Wolverine.MQTT/Internals/MqttTransport.cs +++ b/src/Transports/MQTT/Wolverine.MQTT/Internals/MqttTransport.cs @@ -1,5 +1,6 @@ using ImTools; using JasperFx.Core; +using JasperFx.Descriptors; using Microsoft.Extensions.Logging; using MQTTnet; using MQTTnet.Client; @@ -15,6 +16,7 @@ namespace Wolverine.MQTT.Internals; public class MqttTransport : TransportBase, IAsyncDisposable { + [IgnoreDescription] public LightweightCache Topics { get; } = new(); private List _listeners = new(); private ImHashMap _topicListeners = ImHashMap.Empty; @@ -164,6 +166,7 @@ internal bool tryFindListener(string topicName, out MqttListener listener) internal IManagedMqttClient Client { get; private set; } = null!; internal MqttJwtAuthenticationOptions? JwtAuthenticationOptions { get; set; } + [ChildDescription] public ManagedMqttClientOptions Options { get; set; } = new ManagedMqttClientOptions { ClientOptions = new MqttClientOptions() }; diff --git a/src/Transports/MQTT/Wolverine.MQTT/MqttTopic.cs b/src/Transports/MQTT/Wolverine.MQTT/MqttTopic.cs index 334b633c5..1702038b8 100644 --- a/src/Transports/MQTT/Wolverine.MQTT/MqttTopic.cs +++ b/src/Transports/MQTT/Wolverine.MQTT/MqttTopic.cs @@ -1,3 +1,4 @@ +using JasperFx.Descriptors; using Microsoft.Extensions.Logging; using MQTTnet; using MQTTnet.Extensions.ManagedClient; @@ -42,6 +43,7 @@ public MqttTopic(string topicName, MqttTransport parent, EndpointRole role) : ba Mode = EndpointMode.BufferedInMemory; } + [IgnoreDescription] public MqttTransport Parent { get; } /// @@ -55,6 +57,7 @@ public MqttTopic(string topicName, MqttTransport parent, EndpointRole role) : ba /// When set, overrides the built in envelope mapping with a custom /// implementation /// + [IgnoreDescription] public IMqttEnvelopeMapper EnvelopeMapper { get; set; } public override bool AutoStartSendingAgent() diff --git a/src/Transports/NATS/Wolverine.Nats/Internal/NatsEndpoint.cs b/src/Transports/NATS/Wolverine.Nats/Internal/NatsEndpoint.cs index 4546f1640..abbb9b177 100644 --- a/src/Transports/NATS/Wolverine.Nats/Internal/NatsEndpoint.cs +++ b/src/Transports/NATS/Wolverine.Nats/Internal/NatsEndpoint.cs @@ -1,3 +1,4 @@ +using JasperFx.Descriptors; using Microsoft.Extensions.Logging; using NATS.Client.Core; using NATS.Client.JetStream.Models; @@ -27,6 +28,7 @@ public NatsEndpoint(string subject, NatsTransport transport, EndpointRole role) } public string Subject { get; } + [IgnoreDescription] public object? NatsSerializer { get; set; } public Dictionary CustomHeaders { get; set; } = new(); public string? QueueGroup { get; set; } diff --git a/src/Transports/NATS/Wolverine.Nats/Internal/NatsTransport.cs b/src/Transports/NATS/Wolverine.Nats/Internal/NatsTransport.cs index 3f758aa2e..887ca13e6 100644 --- a/src/Transports/NATS/Wolverine.Nats/Internal/NatsTransport.cs +++ b/src/Transports/NATS/Wolverine.Nats/Internal/NatsTransport.cs @@ -1,3 +1,4 @@ +using JasperFx.Descriptors; using Microsoft.Extensions.Logging; using NATS.Client.Core; using NATS.Client.JetStream; @@ -49,6 +50,7 @@ public NatsTransport() public string ResponseSubject { get; private set; } = "wolverine.response"; + [ChildDescription] public NatsTransportConfiguration Configuration { get; } = new(); public NatsConnection Connection => diff --git a/src/Transports/Pulsar/Wolverine.Pulsar/DeadLetterTopic.cs b/src/Transports/Pulsar/Wolverine.Pulsar/DeadLetterTopic.cs index 705699026..2c49d4a7e 100644 --- a/src/Transports/Pulsar/Wolverine.Pulsar/DeadLetterTopic.cs +++ b/src/Transports/Pulsar/Wolverine.Pulsar/DeadLetterTopic.cs @@ -39,6 +39,11 @@ public string? TopicName set => _topicName = value ?? throw new ArgumentNullException(nameof(TopicName)); } + /// + /// Used by OptionsDescription to render this as the bare topic name. + /// + public override string ToString() => _topicName ?? string.Empty; + protected bool Equals(DeadLetterTopic other) { return _topicName == other._topicName; diff --git a/src/Transports/Pulsar/Wolverine.Pulsar/PulsarTransport.cs b/src/Transports/Pulsar/Wolverine.Pulsar/PulsarTransport.cs index 1c52055d6..2aaf054fa 100644 --- a/src/Transports/Pulsar/Wolverine.Pulsar/PulsarTransport.cs +++ b/src/Transports/Pulsar/Wolverine.Pulsar/PulsarTransport.cs @@ -1,6 +1,7 @@ using DotPulsar; using DotPulsar.Abstractions; using JasperFx.Core; +using JasperFx.Descriptors; using Wolverine.Configuration; using Wolverine.Runtime; using Wolverine.Transports; @@ -23,6 +24,7 @@ public PulsarTransport() : base(ProtocolName, "Pulsar", ["pulsar"]) public PulsarEndpoint this[Uri uri] => _endpoints[uri]; + [IgnoreDescription] public IPulsarClientBuilder Builder { get; } internal IPulsarClient? Client { get; private set; } diff --git a/src/Transports/Pulsar/Wolverine.Pulsar/RetryLetterTopic.cs b/src/Transports/Pulsar/Wolverine.Pulsar/RetryLetterTopic.cs index f286cf0ee..b0f40f354 100644 --- a/src/Transports/Pulsar/Wolverine.Pulsar/RetryLetterTopic.cs +++ b/src/Transports/Pulsar/Wolverine.Pulsar/RetryLetterTopic.cs @@ -39,6 +39,11 @@ public string? TopicName set => _topicName = value ?? throw new ArgumentNullException(nameof(TopicName)); } + /// + /// Used by OptionsDescription to render this as the bare topic name. + /// + public override string ToString() => _topicName ?? string.Empty; + public List Retry => _retries.ToList(); protected bool Equals(RetryLetterTopic other) diff --git a/src/Transports/RabbitMQ/Wolverine.RabbitMQ/DeadLetterQueue.cs b/src/Transports/RabbitMQ/Wolverine.RabbitMQ/DeadLetterQueue.cs index 4c9051ba5..570853032 100644 --- a/src/Transports/RabbitMQ/Wolverine.RabbitMQ/DeadLetterQueue.cs +++ b/src/Transports/RabbitMQ/Wolverine.RabbitMQ/DeadLetterQueue.cs @@ -74,6 +74,13 @@ public DeadLetterQueue Clone() }; } + public override string ToString() + { + // Used by OptionsDescription to render the value as the bare queue name + // rather than the default type-qualified name. + return _queueName; + } + protected bool Equals(DeadLetterQueue other) { return _queueName == other._queueName && ExchangeName == other.ExchangeName; diff --git a/src/Transports/RabbitMQ/Wolverine.RabbitMQ/Internal/RabbitMqConnectionDescription.cs b/src/Transports/RabbitMQ/Wolverine.RabbitMQ/Internal/RabbitMqConnectionDescription.cs new file mode 100644 index 000000000..a42d509a9 --- /dev/null +++ b/src/Transports/RabbitMQ/Wolverine.RabbitMQ/Internal/RabbitMqConnectionDescription.cs @@ -0,0 +1,53 @@ +using JasperFx.Descriptors; +using RabbitMQ.Client; + +namespace Wolverine.RabbitMQ.Internal; + +/// +/// Secret-safe description of a for use with +/// . Exposes non-secret connection fields +/// (host, port, vhost, username, SSL, heartbeat) and deliberately omits the +/// password and any credential-carrying properties. +/// +public sealed class RabbitMqConnectionDescription : IDescribeMyself +{ + private readonly ConnectionFactory _factory; + + public RabbitMqConnectionDescription(ConnectionFactory factory) + { + _factory = factory; + } + + public OptionsDescription ToDescription() + { + var description = new OptionsDescription + { + Subject = "Wolverine.RabbitMQ.ConnectionFactory", + Title = "Connection" + }; + + description.AddValue(nameof(_factory.HostName), _factory.HostName); + description.AddValue(nameof(_factory.Port), _factory.Port); + description.AddValue(nameof(_factory.VirtualHost), _factory.VirtualHost); + description.AddValue(nameof(_factory.UserName), _factory.UserName); + // Password intentionally omitted. + + description.AddValue(nameof(_factory.RequestedHeartbeat), _factory.RequestedHeartbeat); + description.AddValue(nameof(_factory.RequestedConnectionTimeout), _factory.RequestedConnectionTimeout); + description.AddValue(nameof(_factory.ClientProvidedName), _factory.ClientProvidedName ?? string.Empty); + description.AddValue(nameof(_factory.AutomaticRecoveryEnabled), _factory.AutomaticRecoveryEnabled); + description.AddValue(nameof(_factory.TopologyRecoveryEnabled), _factory.TopologyRecoveryEnabled); + + if (_factory.Ssl != null) + { + description.AddValue("Ssl.Enabled", _factory.Ssl.Enabled); + if (_factory.Ssl.Enabled) + { + description.AddValue("Ssl.ServerName", _factory.Ssl.ServerName ?? string.Empty); + description.AddValue("Ssl.Version", _factory.Ssl.Version.ToString()); + } + } + + return description; + } +} diff --git a/src/Transports/RabbitMQ/Wolverine.RabbitMQ/Internal/RabbitMqExchange.cs b/src/Transports/RabbitMQ/Wolverine.RabbitMQ/Internal/RabbitMqExchange.cs index 86ecf2fe9..ce3cc05f3 100644 --- a/src/Transports/RabbitMQ/Wolverine.RabbitMQ/Internal/RabbitMqExchange.cs +++ b/src/Transports/RabbitMQ/Wolverine.RabbitMQ/Internal/RabbitMqExchange.cs @@ -1,4 +1,5 @@ using JasperFx.Core; +using JasperFx.Descriptors; using Microsoft.Extensions.Logging; using RabbitMQ.Client; using Wolverine.Configuration; @@ -32,11 +33,13 @@ internal RabbitMqExchange(string name, RabbitMqTransport parent) /// /// All active topic endpoints by name /// + [IgnoreDescription] public LightweightCache Topics { get; } - + /// /// All active routing keys /// + [IgnoreDescription] public LightweightCache Routings { get; } public override bool AutoStartSendingAgent() @@ -57,6 +60,7 @@ public override bool AutoStartSendingAgent() public ExchangeType ExchangeType { get; set; } = ExchangeType.Fanout; public bool AutoDelete { get; set; } = false; + [IgnoreDescription] public IDictionary Arguments { get; } = new Dictionary(); internal bool HasExchangeBindings => _exchangeBindings.Count > 0; diff --git a/src/Transports/RabbitMQ/Wolverine.RabbitMQ/Internal/RabbitMqQueue.cs b/src/Transports/RabbitMQ/Wolverine.RabbitMQ/Internal/RabbitMqQueue.cs index eb54434d4..d4bcd1056 100644 --- a/src/Transports/RabbitMQ/Wolverine.RabbitMQ/Internal/RabbitMqQueue.cs +++ b/src/Transports/RabbitMQ/Wolverine.RabbitMQ/Internal/RabbitMqQueue.cs @@ -1,3 +1,4 @@ +using JasperFx.Descriptors; using Microsoft.Extensions.Logging; using RabbitMQ.Client; using RabbitMQ.Client.Exceptions; @@ -201,11 +202,13 @@ await _parent.WithAdminChannelAsync(async channel => /// Arguments for Rabbit MQ queue declarations. See the Rabbit MQ .NET client documentation at /// https://www.rabbitmq.com/dotnet.html /// + [IgnoreDescription] public IDictionary Arguments { get; } = new Dictionary(); /// /// Arguments for Rabbit MQ channel consume operations /// + [IgnoreDescription] public IDictionary ConsumerArguments { get; } = new Dictionary(); /// diff --git a/src/Transports/RabbitMQ/Wolverine.RabbitMQ/Internal/RabbitMqTransport.cs b/src/Transports/RabbitMQ/Wolverine.RabbitMQ/Internal/RabbitMqTransport.cs index ec1d5c77d..5591a2f2b 100644 --- a/src/Transports/RabbitMQ/Wolverine.RabbitMQ/Internal/RabbitMqTransport.cs +++ b/src/Transports/RabbitMQ/Wolverine.RabbitMQ/Internal/RabbitMqTransport.cs @@ -1,5 +1,6 @@ using System.Diagnostics; using JasperFx.Core; +using JasperFx.Descriptors; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; @@ -100,10 +101,21 @@ private void configureDefaults(ConnectionFactory factory) /// Allows users to modify properties and behaviors of channels used in RabbitMQ communication /// by providing a delegate to apply specific settings. /// + [ChildDescription] public Action? ChannelCreationOptions { get; set; } + [IgnoreDescription] public ConnectionFactory? ConnectionFactory { get; private set; } + /// + /// Secret-safe summary of for diagnostic + /// rendering. Exposes host / port / vhost / user / SSL / heartbeat and + /// omits the password. + /// + [ChildDescription] + public RabbitMqConnectionDescription? ConnectionDescription => + ConnectionFactory == null ? null : new RabbitMqConnectionDescription(ConnectionFactory); + internal void ConfigureFactory(Action configure) { var factory = new ConnectionFactory @@ -118,11 +130,15 @@ internal void ConfigureFactory(Action configure) ConnectionFactory = factory; } + [IgnoreDescription] public IList AmqpTcpEndpoints { get; } = new List(); + [IgnoreDescription] public LightweightCache Topics { get; } + [IgnoreDescription] public LightweightCache Exchanges { get; } + [IgnoreDescription] public LightweightCache Queues { get; } internal bool DeclareRequestReplySystemQueue { get; set; } = true; @@ -336,6 +352,7 @@ internal ConnectionMonitor BuildConnection(ConnectionRole role) return new ConnectionMonitor(this, role); } + [IgnoreDescription] public ILogger Logger { get; private set; } = NullLogger.Instance; /// diff --git a/src/Transports/Redis/Wolverine.Redis/Internal/RedisTransport.cs b/src/Transports/Redis/Wolverine.Redis/Internal/RedisTransport.cs index 972633fcf..3c7e7bd9f 100644 --- a/src/Transports/Redis/Wolverine.Redis/Internal/RedisTransport.cs +++ b/src/Transports/Redis/Wolverine.Redis/Internal/RedisTransport.cs @@ -1,6 +1,7 @@ using System.Collections.Concurrent; using System.Linq; using JasperFx.Core; +using JasperFx.Descriptors; using Microsoft.Extensions.Logging; using StackExchange.Redis; using Spectre.Console; @@ -34,6 +35,7 @@ public class RedisTransport : BrokerTransport, IAsyncDispos /// Customizable selector to build a stable consumer name for listeners when an endpoint-level ConsumerName is not set. /// Defaults to ServiceName-NodeNumber-MachineName (lowercased and sanitized). /// + [DescribeAsConfigurationState] public Func? DefaultConsumerNameSelector { get; set; } /// @@ -71,16 +73,22 @@ public override Uri ResourceUri // Parse connection string to build resource URI var options = ConfigurationOptions.Parse(_connectionString); var endpoint = options.EndPoints.FirstOrDefault(); - + if (endpoint == null) { return new Uri($"{ProtocolName}://localhost:6379"); } - + return new Uri($"{ProtocolName}://{endpoint}"); } } + /// + /// The configured Redis connection string with the password value + /// masked. Safe to render in diagnostic output. + /// + public string ConnectionSummary => SanitizeConnectionStringForLogging(_connectionString); + internal IDatabase GetDatabase(string? connectionString = null, int database = 0) { var connection = GetConnection(connectionString); diff --git a/src/Transports/SignalR/Wolverine.SignalR/Client/SignalRClientEndpoint.cs b/src/Transports/SignalR/Wolverine.SignalR/Client/SignalRClientEndpoint.cs index 5df9b9d7e..e9c690853 100644 --- a/src/Transports/SignalR/Wolverine.SignalR/Client/SignalRClientEndpoint.cs +++ b/src/Transports/SignalR/Wolverine.SignalR/Client/SignalRClientEndpoint.cs @@ -1,4 +1,5 @@ using JasperFx.Core; +using JasperFx.Descriptors; using Microsoft.AspNetCore.SignalR; using Microsoft.AspNetCore.SignalR.Client; using Microsoft.Extensions.Logging; @@ -37,8 +38,10 @@ public SignalRClientEndpoint(Uri uri, SignalRClientTransport parent) : base(Tran JsonOptions = new SignalRTransport().JsonOptions; } + [IgnoreDescription] public JsonSerializerOptions JsonOptions { get; set; } + [DescribeAsConfigurationState] public Func>> AccessTokenProvider { get; set; } = null!; public Uri SignalRUri { get; } @@ -105,6 +108,7 @@ internal async Task ReceiveAsync(string json) } } + [IgnoreDescription] public IReceiver? Receiver { get; private set; } protected override ISender CreateSender(IWolverineRuntime runtime) @@ -119,6 +123,7 @@ protected override ISender CreateSender(IWolverineRuntime runtime) return this; } + [IgnoreDescription] public IHandlerPipeline? Pipeline { get; private set; } ValueTask IChannelCallback.CompleteAsync(Envelope envelope) diff --git a/src/Transports/SignalR/Wolverine.SignalR/Internals/SignalRTransport.cs b/src/Transports/SignalR/Wolverine.SignalR/Internals/SignalRTransport.cs index 771733d98..721413c23 100644 --- a/src/Transports/SignalR/Wolverine.SignalR/Internals/SignalRTransport.cs +++ b/src/Transports/SignalR/Wolverine.SignalR/Internals/SignalRTransport.cs @@ -1,6 +1,7 @@ using System.Text.Json; using System.Text.Json.Serialization; using JasperFx.Core; +using JasperFx.Descriptors; using JasperFx.Resources; using Microsoft.AspNetCore.SignalR; using Microsoft.Extensions.DependencyInjection; @@ -69,6 +70,7 @@ ValueTask ITransport.InitializeAsync(IWolverineRuntime runtime) return new ValueTask(); } + [IgnoreDescription] public IHubContext? HubContext { get; private set; } public Type HubType { get; internal set; } = typeof(WolverineHub); @@ -80,8 +82,10 @@ bool ITransport.TryBuildStatefulResource(IWolverineRuntime runtime, out IStatefu internal ILogger? Logger { get; set; } + [IgnoreDescription] public JsonSerializerOptions JsonOptions { get; set; } + [IgnoreDescription] public IReceiver? Receiver { get; private set; } internal async Task ReceiveAsync(HubCallerContext context, string json) @@ -116,6 +120,7 @@ public override ValueTask BuildListenerAsync(IWolverineRuntime runtim return new ValueTask(this); } + [IgnoreDescription] public IHandlerPipeline? Pipeline => Receiver?.Pipeline; ValueTask IChannelCallback.CompleteAsync(Envelope envelope) diff --git a/src/Wolverine/ErrorHandling/FailureRule.cs b/src/Wolverine/ErrorHandling/FailureRule.cs index 2e7b01022..98107827a 100644 --- a/src/Wolverine/ErrorHandling/FailureRule.cs +++ b/src/Wolverine/ErrorHandling/FailureRule.cs @@ -55,4 +55,23 @@ public FailureSlot AddSlot(IContinuationSource source) return slot; } + + public override string ToString() + { + var parts = new List(_slots.Count + 1); + + foreach (var slot in _slots) + { + parts.Add($"attempt {slot.Attempt}: {slot.Describe()}"); + } + + if (InfiniteSource != null) + { + var prefix = _slots.Count > 0 ? "then repeat" : "repeat"; + parts.Add($"{prefix}: {InfiniteSource.Description}"); + } + + var actions = parts.Count > 0 ? string.Join("; ", parts) : "no action"; + return $"On {Match.Description} \u2014 {actions}"; + } } \ No newline at end of file diff --git a/src/Wolverine/ErrorHandling/SendingFailurePolicies.cs b/src/Wolverine/ErrorHandling/SendingFailurePolicies.cs index c8ccbcc35..6b92b53d5 100644 --- a/src/Wolverine/ErrorHandling/SendingFailurePolicies.cs +++ b/src/Wolverine/ErrorHandling/SendingFailurePolicies.cs @@ -1,3 +1,4 @@ +using JasperFx.Descriptors; using Wolverine.Runtime; namespace Wolverine.ErrorHandling; @@ -7,8 +8,15 @@ namespace Wolverine.ErrorHandling; /// Unlike handler failure policies, unmatched exceptions return null /// so the existing retry/circuit-breaker behavior is preserved. /// -public class SendingFailurePolicies : IWithFailurePolicies +public class SendingFailurePolicies : IWithFailurePolicies, IOptionsValueAsStringArray { + /// + /// Renders the failure rules as a string array so + /// shows one entry per rule instead of the default class ToString(). + /// + public IReadOnlyList ToOptionsValueStrings() + => Failures.Select(rule => rule.ToString()!).ToArray(); + /// /// Collection of error handling policies for exception handling during the sending of a message /// diff --git a/src/Wolverine/Runtime/Handlers/HandlerGraph.ISystemDescribedPart.cs b/src/Wolverine/Runtime/Handlers/HandlerGraph.ISystemDescribedPart.cs index 5e0a3d607..17cd9afec 100644 --- a/src/Wolverine/Runtime/Handlers/HandlerGraph.ISystemDescribedPart.cs +++ b/src/Wolverine/Runtime/Handlers/HandlerGraph.ISystemDescribedPart.cs @@ -62,7 +62,7 @@ internal Task WriteToConsole() } } - AnsiConsole.Render(table); + AnsiConsole.Write(table); } return Task.CompletedTask; diff --git a/src/Wolverine/Runtime/Interop/CloudEventsMapper.cs b/src/Wolverine/Runtime/Interop/CloudEventsMapper.cs index da388f1e1..4929e3613 100644 --- a/src/Wolverine/Runtime/Interop/CloudEventsMapper.cs +++ b/src/Wolverine/Runtime/Interop/CloudEventsMapper.cs @@ -92,6 +92,8 @@ public CloudEventsMapper(HandlerGraph handlers, JsonSerializerOptions options) _options = options; } + public override string ToString() => "Cloud Events"; + public string WriteToString(Envelope envelope) { return JsonSerializer.Serialize(new CloudEventsEnvelope(envelope), _options); diff --git a/src/Wolverine/Transports/BrokerResource.cs b/src/Wolverine/Transports/BrokerResource.cs index a2dac1ee4..1c647c4ff 100644 --- a/src/Wolverine/Transports/BrokerResource.cs +++ b/src/Wolverine/Transports/BrokerResource.cs @@ -95,10 +95,7 @@ public async Task Setup(CancellationToken token) public async Task DetermineStatus(CancellationToken token) { - var table = new Table - { - Alignment = Justify.Left - }; + var table = new Table(); var columns = _transport.DiagnosticColumns().ToArray(); if (columns.Length == 0) return table; diff --git a/src/Wolverine/Transports/ConnectionStringRedactor.cs b/src/Wolverine/Transports/ConnectionStringRedactor.cs new file mode 100644 index 000000000..17ea967e2 --- /dev/null +++ b/src/Wolverine/Transports/ConnectionStringRedactor.cs @@ -0,0 +1,61 @@ +using System.Text; + +namespace Wolverine.Transports; + +/// +/// Small helper for masking credential-bearing keys in a connection string +/// while preserving the rest of the content. Used to render a transport's +/// connection string in +/// output without leaking secrets into logs or diagnostic UIs. +/// +public static class ConnectionStringRedactor +{ + /// + /// Mask the values of any key=value segments whose key (case-insensitive) + /// appears in . Segments are delimited by + /// ;. Unknown keys pass through unchanged. + /// + public static string Redact(string? connectionString, params string[] secretKeys) + { + if (string.IsNullOrWhiteSpace(connectionString)) return string.Empty; + if (secretKeys == null || secretKeys.Length == 0) return connectionString!; + + var builder = new StringBuilder(connectionString!.Length); + var segments = connectionString.Split(';'); + for (var i = 0; i < segments.Length; i++) + { + var segment = segments[i]; + if (segment.Length == 0) continue; + + var equalsIndex = segment.IndexOf('='); + if (equalsIndex > 0) + { + var key = segment.Substring(0, equalsIndex); + if (IsSecretKey(key, secretKeys)) + { + if (builder.Length > 0) builder.Append(';'); + builder.Append(key).Append("=****"); + continue; + } + } + + if (builder.Length > 0) builder.Append(';'); + builder.Append(segment); + } + + return builder.ToString(); + } + + private static bool IsSecretKey(string key, string[] secretKeys) + { + foreach (var secret in secretKeys) + { + if (string.Equals(key.Trim(), secret, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + } + + return false; + } +}