diff --git a/src/EventStore.Plugins/Diagnostics/DiagnosticsListeners.cs b/src/EventStore.Plugins/Diagnostics/DiagnosticsListeners.cs index 9978335..d9e6783 100644 --- a/src/EventStore.Plugins/Diagnostics/DiagnosticsListeners.cs +++ b/src/EventStore.Plugins/Diagnostics/DiagnosticsListeners.cs @@ -1,4 +1,5 @@ using System.Collections; +using System.Collections.Concurrent; using System.Diagnostics; namespace EventStore.Plugins.Diagnostics; @@ -13,7 +14,7 @@ public MultiSourceDiagnosticsListener(string[] sources, int capacity = 10, OnSou foreach (var source in sources) Listeners.TryAdd(source, new(source, capacity, data => onEvent?.Invoke(source, data))); } - + Dictionary Listeners { get; } = new(); public IEnumerable CollectedEvents(string source) => @@ -26,7 +27,7 @@ public void ClearCollectedEvents(string source) { if (Listeners.TryGetValue(source, out var listener)) listener.ClearCollectedEvents(); } - + public void ClearAllCollectedEvents() { foreach (var listener in Listeners.Values) listener.ClearCollectedEvents(); @@ -35,17 +36,17 @@ public void ClearAllCollectedEvents() { public void Dispose() { foreach (var listener in Listeners.Values) listener.Dispose(); - + Listeners.Clear(); } - + public static MultiSourceDiagnosticsListener Start(OnSourceEvent onEvent, params string[] sources) => new(sources, 10, onEvent); - + public static MultiSourceDiagnosticsListener Start(OnSourceEvent onEvent, int capacity, params string[] sources) => new(sources, capacity, onEvent); - - public static MultiSourceDiagnosticsListener Start(params string[] sources) => + + public static MultiSourceDiagnosticsListener Start(params string[] sources) => new(sources); public static MultiSourceDiagnosticsListener Start(int capacity, params string[] sources) => @@ -62,38 +63,38 @@ public SingleSourceDiagnosticsListener(string source, int capacity = 10, Action< onEvent?.Invoke(data.Value); }); } - + GenericDiagnosticsListener Listener { get; } List ValidEvents => Listener.CollectedEvents .Where(x => x.Value is not null) .Select(x => x.Value!) .ToList(); - + public string Source => Listener.Source; public int Capacity => Listener.Capacity; - + public IReadOnlyList CollectedEvents => ValidEvents; - + public bool HasCollectedEvents => Listener.HasCollectedEvents; - + public void ClearCollectedEvents() => Listener.ClearCollectedEvents(); - + public IEnumerator GetEnumerator() => ValidEvents.GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); public void Dispose() => Listener.Dispose(); - - public static SingleSourceDiagnosticsListener Start(string source, int capacity) => + + public static SingleSourceDiagnosticsListener Start(string source, int capacity) => new(source, capacity); - - public static SingleSourceDiagnosticsListener Start(string source) => + + public static SingleSourceDiagnosticsListener Start(string source) => new(source); - + public static SingleSourceDiagnosticsListener Start(Action onEvent, string source) => new(source, 10, onEvent); - + public static SingleSourceDiagnosticsListener Start(Action onEvent, int capacity, string source) => new(source, capacity, onEvent); } @@ -101,23 +102,23 @@ public static SingleSourceDiagnosticsListener Start(Action onEvent, int /// /// Generic listener that also collects the last N events and can be used to subscribe to a single source. /// -class GenericDiagnosticsListener : IDisposable, IEnumerable> { +class GenericDiagnosticsListener : IDisposable { static readonly object Locker = new(); - + public GenericDiagnosticsListener(string source, int capacity = 10, Action>? onEvent = null) { if (string.IsNullOrWhiteSpace(source)) throw new ArgumentException("Source cannot be null or whitespace.", nameof(source)); ArgumentOutOfRangeException.ThrowIfNegative(capacity); - + Source = source; Capacity = capacity; Queue = new(capacity); - + var observer = new GenericObserver>(data => { if (capacity > 0) Queue.Enqueue(data); - + try { onEvent?.Invoke(data); } @@ -140,63 +141,52 @@ void OnNewListener(DiagnosticListener listener) { } } } - - FixedSizedQueue> Queue { get; } + + + FixedSizedConcurrentQueue> Queue { get; } IDisposable? ListenerSubscription { get; } IDisposable? NetworkSubscription { get; set; } - + public string Source { get; } public int Capacity { get; } - public IReadOnlyList> CollectedEvents => Queue.ToList(); - - public bool HasCollectedEvents => Queue.Count != 0; - + public IReadOnlyList> CollectedEvents => Queue.ToArray(); + + public bool HasCollectedEvents => Queue.TryPeek(out _); + public void ClearCollectedEvents() => Queue.Clear(); - + public void Dispose() { NetworkSubscription?.Dispose(); ListenerSubscription?.Dispose(); ClearCollectedEvents(); } - - public IEnumerator> GetEnumerator() => Queue.ToList().GetEnumerator(); - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - class GenericObserver(Action? onNext, Action? onCompleted = null) : IObserver { public void OnNext(T value) => _onNext(value); public void OnCompleted() => _onCompleted(); public void OnError(Exception error) { } - + readonly Action _onNext = onNext ?? (_ => { }); readonly Action _onCompleted = onCompleted ?? (() => { }); } - - class FixedSizedQueue(int maxSize) : Queue { + + class FixedSizedConcurrentQueue(int maxSize) : ConcurrentQueue { readonly object _locker = new(); public new void Enqueue(T item) { lock (_locker) { base.Enqueue(item); if (Count > maxSize) - Dequeue(); // Throw away - } - } - - public new void Clear() { - lock (_locker) { - base.Clear(); + TryDequeue(out _); // Throw away } } } - - public static GenericDiagnosticsListener Start(string source, int capacity = 10, Action>? onEvent = null) => - new(source, capacity, onEvent); - - public static GenericDiagnosticsListener Start(string source, Action>? onEvent = null) => - new(source, 10, onEvent); -} + public static GenericDiagnosticsListener Start(string source, int capacity = 10, Action>? onEvent = null) => + new(source, capacity, onEvent); + public static GenericDiagnosticsListener Start(string source, Action>? onEvent = null) => + new(source, 10, onEvent); +} \ No newline at end of file diff --git a/src/EventStore.Plugins/Plugin.cs b/src/EventStore.Plugins/Plugin.cs index 74fce6b..f1cb94b 100644 --- a/src/EventStore.Plugins/Plugin.cs +++ b/src/EventStore.Plugins/Plugin.cs @@ -34,9 +34,7 @@ protected Plugin( .Replace("Subsystems", "", OrdinalIgnoreCase) .Replace("Subsystem", "", OrdinalIgnoreCase); - Version = version - ?? pluginType.Assembly.GetName().Version?.ToString() - ?? "1.0.0.0-preview"; + Version = GetPluginVersion(version, pluginType); LicensePublicKey = licensePublicKey; @@ -47,6 +45,19 @@ protected Plugin( IsEnabledResult = (true, ""); Configuration = null!; + + return; + + static string GetPluginVersion(string? pluginVersion, Type pluginType) { + const string emptyVersion = "0.0.0.0"; + const string defaultVersion = "1.0.0.0-preview"; + + var version = pluginVersion + ?? pluginType.Assembly.GetName().Version?.ToString() + ?? emptyVersion; + + return version != emptyVersion ? version : defaultVersion; + } } protected Plugin(PluginOptions options) : this( @@ -75,7 +86,7 @@ protected Plugin(PluginOptions options) : this( /// public KeyValuePair[] DiagnosticsTags { get; } - + /// public bool Enabled => IsEnabledResult.Enabled; @@ -89,67 +100,67 @@ public virtual void ConfigureApplication(IApplicationBuilder app, IConfiguration /// /// The configuration of the application.
public virtual (bool Enabled, string EnableInstructions) IsEnabled(IConfiguration configuration) => IsEnabledResult; - + public void Disable(string reason) => IsEnabledResult = (false, reason); - + void IPlugableComponent.ConfigureServices(IServiceCollection services, IConfiguration configuration) { Configuration = configuration; IsEnabledResult = IsEnabled(configuration); - + if (Enabled) ConfigureServices(services, configuration); - + PublishDiagnosticsData(new() { ["enabled"] = Enabled }, Partial); } void IPlugableComponent.ConfigureApplication(IApplicationBuilder app, IConfiguration configuration) { var logger = app.ApplicationServices.GetRequiredService().CreateLogger(GetType()); - + // if the plugin is disabled, just get out if (!Enabled) { logger.LogInformation( - "{Version} plugin disabled. {EnableInstructions}", - Version, IsEnabledResult.EnableInstructions + "{PluginName} {Version} plugin disabled. {EnableInstructions}", + Name, Version, IsEnabledResult.EnableInstructions ); return; } - + // if the plugin is enabled, but the license is invalid, throw an exception and effectivly disable the plugin var license = app.ApplicationServices.GetService(); if (Enabled && LicensePublicKey is not null && (license is null || !license.IsValid(LicensePublicKey))) { var ex = new PluginLicenseException(Name); - + IsEnabledResult = (false, ex.Message); - + PublishDiagnosticsData(new() { ["enabled"] = Enabled }, Partial); - + logger.LogInformation( - "{Version} plugin disabled. {EnableInstructions}", - Version, IsEnabledResult.EnableInstructions + "{PluginName} {Version} plugin disabled. {EnableInstructions}", + Name, Version, IsEnabledResult.EnableInstructions ); - + throw ex; } - + // there is still a chance to disable the plugin when configuring the application // this is useful when the plugin is enabled, but some conditions that can only be checked here are not met ConfigureApplication(app, Configuration); // at this point we know if the plugin is enabled and configured or not if (Enabled) - logger.LogInformation("{Version} plugin enabled.", Version); + logger.LogInformation("{PluginName} {Version} plugin enabled.", Name, Version); else { logger.LogInformation( - "{Version} plugin disabled. {EnableInstructions}", - Version, IsEnabledResult.EnableInstructions + "{PluginName} {Version} plugin disabled. {EnableInstructions}", + Name, Version, IsEnabledResult.EnableInstructions ); } - + // finally publish diagnostics data PublishDiagnosticsData(new() { ["enabled"] = Enabled }, Partial); } - + /// /// Publishes diagnostics data as a snapshot.
/// Uses the container.
@@ -164,7 +175,7 @@ protected internal void PublishDiagnosticsData(Dictionary event Data = eventData, CollectionMode = mode }; - + DiagnosticListener.Write(nameof(PluginDiagnosticsData), value); } @@ -177,9 +188,9 @@ protected internal void PublishDiagnosticsData(Dictionary event /// The data to publish. /// The mode of data collection for a plugin event. protected internal void PublishDiagnosticsData(string eventName, Dictionary eventData, PluginDiagnosticsDataCollectionMode mode = Event) { - if (eventName == nameof(PluginDiagnosticsData)) + if (eventName == nameof(PluginDiagnosticsData)) throw new ArgumentException("Event name cannot be PluginDiagnosticsData", nameof(eventName)); - + DiagnosticListener.Write( eventName, new PluginDiagnosticsData{ @@ -190,7 +201,7 @@ protected internal void PublishDiagnosticsData(string eventName, Dictionary /// Publishes diagnostics events.
///
@@ -201,4 +212,4 @@ protected internal void PublishDiagnosticsEvent(T pluginEvent) => /// public void Dispose() => DiagnosticListener.Dispose(); -} +} \ No newline at end of file diff --git a/test/EventStore.Plugins.Tests/PluginBaseTests.cs b/test/EventStore.Plugins.Tests/PluginBaseTests.cs index a785adf..0ef5af1 100644 --- a/test/EventStore.Plugins.Tests/PluginBaseTests.cs +++ b/test/EventStore.Plugins.Tests/PluginBaseTests.cs @@ -23,7 +23,7 @@ public void plugin_base_sets_defaults_automatically() { plugin.Options.Should().BeEquivalentTo(expectedOptions); } - + [Fact] public void subsystems_plugin_base_sets_defaults_automatically() { var expectedOptions = new SubsystemsPluginOptions { @@ -37,7 +37,7 @@ public void subsystems_plugin_base_sets_defaults_automatically() { plugin.Options.Should().BeEquivalentTo(expectedOptions); } - + [Fact] public void plugin_diagnostics_snapshot_is_not_overriden_internally() { // Arrange @@ -45,23 +45,23 @@ public void plugin_diagnostics_snapshot_is_not_overriden_internally() { ["first_value"] = 1, ["second_value"] = 2 }; - + IPlugableComponent plugin = new NightCityPlugin(new(){ Name = Guid.NewGuid().ToString() }) { OnConfigureServices = x => x.PublishDiagnosticsData(userDiagnosticsData), OnConfigureApplication = x => x.Disable("Disabled on ConfigureApplication because I can") }; - + using var collector = PluginDiagnosticsDataCollector.Start(plugin.DiagnosticsName); var builder = WebApplication.CreateBuilder(); - + plugin.ConfigureServices(builder.Services, builder.Configuration); - + using var app = builder.Build(); // Act & Assert plugin.ConfigureApplication(app, app.Configuration); - + var expectedDiagnosticsData = new Dictionary(userDiagnosticsData) { ["enabled"] = false }; @@ -69,7 +69,7 @@ public void plugin_diagnostics_snapshot_is_not_overriden_internally() { collector.CollectedEvents(plugin.DiagnosticsName).Should().ContainSingle() .Which.Data.Should().BeEquivalentTo(expectedDiagnosticsData); } - + [Fact] public void comercial_plugin_is_disabled_when_licence_is_missing() { // Arrange @@ -137,25 +137,25 @@ public void comercial_plugin_is_enabled_when_licence_is_present() { // Act & Assert configure.Should().NotThrow(); } - + [Fact] public void plugin_can_be_disabled_on_ConfigureServices() { // Arrange IPlugableComponent plugin = new NightCityPlugin(new(){ Name = Guid.NewGuid().ToString() }) { OnConfigureServices = x => x.Disable("Disabled on ConfigureServices because I can") }; - + using var collector = PluginDiagnosticsDataCollector.Start(plugin.DiagnosticsName); var builder = WebApplication.CreateBuilder(); // Act & Assert plugin.Enabled.Should().BeTrue(); - + plugin.ConfigureServices(builder.Services, builder.Configuration); - + plugin.Enabled.Should().BeFalse(); - + collector.CollectedEvents(plugin.DiagnosticsName).Should().ContainSingle() .Which.Data.Should().ContainKey("enabled") .WhoseValue.Should().BeEquivalentTo(false); @@ -167,22 +167,22 @@ public void plugin_can_be_disabled_on_ConfigureApplication() { IPlugableComponent plugin = new NightCityPlugin(new(){ Name = Guid.NewGuid().ToString() }) { OnConfigureApplication = x => x.Disable("Disabled on ConfigureApplication because I can") }; - + using var collector = PluginDiagnosticsDataCollector.Start(plugin.DiagnosticsName); var builder = WebApplication.CreateBuilder(); - + plugin.ConfigureServices(builder.Services, builder.Configuration); - + using var app = builder.Build(); // Act & Assert plugin.Enabled.Should().BeTrue(); - + plugin.ConfigureApplication(app, app.Configuration); - + plugin.Enabled.Should().BeFalse(); - + collector.CollectedEvents(plugin.DiagnosticsName).Should().ContainSingle() .Which.Data.Should().ContainKey("enabled") .WhoseValue.Should().BeEquivalentTo(false); @@ -212,11 +212,11 @@ public NightCityPlugin() : this(new()) { } public Action? OnConfigureServices { get; set; } public Action? OnConfigureApplication { get; set; } - - public override void ConfigureServices(IServiceCollection services, IConfiguration configuration) => + + public override void ConfigureServices(IServiceCollection services, IConfiguration configuration) => OnConfigureServices?.Invoke(this); - public override void ConfigureApplication(IApplicationBuilder app, IConfiguration configuration) => + public override void ConfigureApplication(IApplicationBuilder app, IConfiguration configuration) => OnConfigureApplication?.Invoke(this); }