diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9990a5e --- /dev/null +++ b/.gitignore @@ -0,0 +1,114 @@ +.vs + +# Build Folders (you can keep bin if you'd like, to store dlls and pdbs) +[Bb]in/ +[Oo]bj/ +[Dd]eployment + +# mstest test results +TestResults + +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.sln.docstates + +# Build results +[Dd]ebug/ +[Rr]elease/ +x64/ +*_i.c +*_p.c +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.log +*.vspscc +*.vssscc +.builds + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opensdf +*.sdf + +# Visual Studio profiler +*.psess +*.vsp +*.vspx + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user +*.DotSettings + +# NCrunch +*.ncrunch* +.*crunch*.local.xml + +# Installshield output folder +[Ee]xpress + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish + +# Publish Web Output +*.Publish.xml + +# NuGet Packages Directory +packages + +# Windows Azure Build Output +csx +*.build.csdef + +# Windows Store app package directory +AppPackages/ + +# Others +[Bb]in +[Oo]bj +sql +TestResults +[Tt]est[Rr]esult* +*.Cache +ClientBin +[Ss]tyle[Cc]op.* +~$* +*.dbmdl +Generated_Code #added for RIA/Silverlight projects + +# Backup & report files from converting an old project file to a newer +# Visual Studio version. Backup files are not needed, because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML diff --git a/.media/logo.png b/.media/logo.png new file mode 100644 index 0000000..3af388d Binary files /dev/null and b/.media/logo.png differ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..21736fc --- /dev/null +++ b/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2024, Eben Roux +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + Redistributions in binary form must reproduce the above copyright notice, this + list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + Neither the name of the Shuttle organization nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..4fd7765 --- /dev/null +++ b/README.md @@ -0,0 +1,25 @@ +# Recall Logging + +``` +PM> Install-Package Shuttle.Recall.Logging +``` + +## Configuration + +```c# +services.AddRecallLogging(); // all logging options enabled +``` + +Specific logging options may be specified: + +```c# +services.AddRecallLogging(builder => +{ + builder.Options.PipelineTypes = new List { "pieline-type-name" }; + builder.Options.PipelineEventTypes = new List { "pieline-event-type-name" }; + builder.Options.AddPipelineType(); + builder.Options.AddPipelineType(pipelineType); + builder.Options.AddPipelineEventType(); + builder.Options.AddPipelineEventType(pipelineEventType); +}); +``` \ No newline at end of file diff --git a/Shuttle.Recall.Logging.sln b/Shuttle.Recall.Logging.sln new file mode 100644 index 0000000..68f4e7a --- /dev/null +++ b/Shuttle.Recall.Logging.sln @@ -0,0 +1,31 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.9.34622.214 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Shuttle.Recall.Logging", "Shuttle.Recall.Logging\Shuttle.Recall.Logging.csproj", "{F24CBCDF-F116-4005-9AD5-E58E50B62A27}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{3B2973EF-D99B-40BA-8534-36CB21D6E1DF}" + ProjectSection(SolutionItems) = preProject + LICENSE = LICENSE + README.md = README.md + EndProjectSection +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {F24CBCDF-F116-4005-9AD5-E58E50B62A27}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F24CBCDF-F116-4005-9AD5-E58E50B62A27}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F24CBCDF-F116-4005-9AD5-E58E50B62A27}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F24CBCDF-F116-4005-9AD5-E58E50B62A27}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {6DD7470E-DB82-45D5-A1F8-4C63FD8377CC} + EndGlobalSection +EndGlobal diff --git a/Shuttle.Recall.Logging/.package/AssemblyInfo.cs.template b/Shuttle.Recall.Logging/.package/AssemblyInfo.cs.template new file mode 100644 index 0000000..15b6466 --- /dev/null +++ b/Shuttle.Recall.Logging/.package/AssemblyInfo.cs.template @@ -0,0 +1,22 @@ +using System.Reflection; +using System.Runtime.InteropServices; + +#if NETFRAMEWORK +[assembly: AssemblyTitle(".NET Framework")] +#endif + +#if NETCOREAPP +[assembly: AssemblyTitle(".NET Core")] +#endif + +#if NETSTANDARD +[assembly: AssemblyTitle(".NET Standard")] +#endif + +[assembly: AssemblyVersion("#{SemanticVersionCore}#.0")] +[assembly: AssemblyCopyright("Copyright (c) #{Year}#, Eben Roux")] +[assembly: AssemblyProduct("Shuttle.Recall.Logging")] +[assembly: AssemblyCompany("Eben Roux")] +[assembly: AssemblyConfiguration("Release")] +[assembly: AssemblyInformationalVersion("#{SemanticVersion}#")] +[assembly: ComVisible(false)] \ No newline at end of file diff --git a/Shuttle.Recall.Logging/.package/Shuttle.NuGetPackager.MSBuild.dll b/Shuttle.Recall.Logging/.package/Shuttle.NuGetPackager.MSBuild.dll new file mode 100644 index 0000000..d746a0e Binary files /dev/null and b/Shuttle.Recall.Logging/.package/Shuttle.NuGetPackager.MSBuild.dll differ diff --git a/Shuttle.Recall.Logging/.package/Shuttle.NuGetPackager.targets b/Shuttle.Recall.Logging/.package/Shuttle.NuGetPackager.targets new file mode 100644 index 0000000..d714aa8 --- /dev/null +++ b/Shuttle.Recall.Logging/.package/Shuttle.NuGetPackager.targets @@ -0,0 +1,12 @@ + + + + Shuttle.NuGetPackager.MSBuild.dll + + + + + + + + diff --git a/Shuttle.Recall.Logging/.package/package.msbuild b/Shuttle.Recall.Logging/.package/package.msbuild new file mode 100644 index 0000000..4c7d4ac --- /dev/null +++ b/Shuttle.Recall.Logging/.package/package.msbuild @@ -0,0 +1,104 @@ + + + Shuttle.Recall.Logging + https://www.nuget.org + Release + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Shuttle.Recall.Logging/.package/package.nuspec b/Shuttle.Recall.Logging/.package/package.nuspec new file mode 100644 index 0000000..803b681 --- /dev/null +++ b/Shuttle.Recall.Logging/.package/package.nuspec @@ -0,0 +1,27 @@ + + + + + Shuttle.Recall.Logging + 17.0.0 + Eben Roux + Eben Roux + BSD-3-Clause + false + images\logo.png + docs\README.md + + https://github.com/Shuttle/Shuttle.Recall.Logging + Provides non-intrusive logging for Shuttle.Recall components. + Copyright (c) 2024, Eben Roux + + + + + + + + + + + diff --git a/Shuttle.Recall.Logging/.package/package.nuspec.template b/Shuttle.Recall.Logging/.package/package.nuspec.template new file mode 100644 index 0000000..fc801aa --- /dev/null +++ b/Shuttle.Recall.Logging/.package/package.nuspec.template @@ -0,0 +1,27 @@ + + + + + Shuttle.Recall.Logging + #{SemanticVersion}# + Eben Roux + Eben Roux + BSD-3-Clause + false + images\logo.png + docs\README.md + + https://github.com/Shuttle/Shuttle.Recall.Logging + Provides non-intrusive logging for Shuttle.Recall components. + Copyright (c) #{Year}#, Eben Roux + + +#{Dependencies}# + + + + + + + + diff --git a/Shuttle.Recall.Logging/EventProcessingPipelineLogger.cs b/Shuttle.Recall.Logging/EventProcessingPipelineLogger.cs new file mode 100644 index 0000000..7fe24e1 --- /dev/null +++ b/Shuttle.Recall.Logging/EventProcessingPipelineLogger.cs @@ -0,0 +1,57 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Shuttle.Core.Contract; +using Shuttle.Core.Pipelines; + +namespace Shuttle.Recall.Logging +{ + public class EventProcessingPipelineLogger : IHostedService + { + private readonly Type _pipelineType = typeof(EventProcessingPipeline); + private readonly ILogger _logger; + private readonly IPipelineFactory _pipelineFactory; + private readonly IRecallLoggingConfiguration _recallLoggingConfiguration; + + public EventProcessingPipelineLogger(ILogger logger, IRecallLoggingConfiguration recallLoggingConfiguration, IPipelineFactory pipelineFactory) + { + _logger = Guard.AgainstNull(logger, nameof(logger)); + _recallLoggingConfiguration = Guard.AgainstNull(recallLoggingConfiguration, nameof(recallLoggingConfiguration)); + _pipelineFactory = Guard.AgainstNull(pipelineFactory, nameof(pipelineFactory)); + + if (_recallLoggingConfiguration.ShouldLogPipelineType(_pipelineType)) + { + _pipelineFactory.PipelineCreated += OnPipelineCreated; + + } + } + + public async Task StartAsync(CancellationToken cancellationToken) + { + await Task.CompletedTask; + } + + private void OnPipelineCreated(object sender, PipelineEventArgs args) + { + if (args.Pipeline.GetType() != _pipelineType) + { + return; + } + + args.Pipeline.RegisterObserver(new EventProcessingPipelineObserver(_logger, _recallLoggingConfiguration)); + } + + public async Task StopAsync(CancellationToken cancellationToken) + { + if (_recallLoggingConfiguration.ShouldLogPipelineType(_pipelineType)) + { + _pipelineFactory.PipelineCreated -= OnPipelineCreated; + + } + + await Task.CompletedTask; + } + } +} \ No newline at end of file diff --git a/Shuttle.Recall.Logging/EventProcessingPipelineObserver.cs b/Shuttle.Recall.Logging/EventProcessingPipelineObserver.cs new file mode 100644 index 0000000..ec75a93 --- /dev/null +++ b/Shuttle.Recall.Logging/EventProcessingPipelineObserver.cs @@ -0,0 +1,151 @@ +using System; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Shuttle.Core.Contract; +using Shuttle.Core.Pipelines; +using Shuttle.Core.PipelineTransaction; + +namespace Shuttle.Recall.Logging +{ + public class EventProcessingPipelineObserver : PipelineObserver, + IPipelineObserver, + IPipelineObserver, + IPipelineObserver, + IPipelineObserver, + IPipelineObserver, + IPipelineObserver, + IPipelineObserver, + IPipelineObserver, + IPipelineObserver, + IPipelineObserver, + IPipelineObserver, + IPipelineObserver + { + public EventProcessingPipelineObserver(ILogger logger, IRecallLoggingConfiguration recallLoggingConfiguration) + : base(logger, recallLoggingConfiguration) + { + } + + public void Execute(OnStartTransactionScope pipelineEvent) + { + Trace(pipelineEvent).GetAwaiter().GetResult(); + } + + public async Task ExecuteAsync(OnStartTransactionScope pipelineEvent) + { + await Trace(pipelineEvent); + } + + public void Execute(OnAfterStartTransactionScope pipelineEvent) + { + Trace(pipelineEvent).GetAwaiter().GetResult(); + } + + public async Task ExecuteAsync(OnAfterStartTransactionScope pipelineEvent) + { + await Trace(pipelineEvent); + } + + public void Execute(OnGetProjectionEvent pipelineEvent) + { + Trace(pipelineEvent).GetAwaiter().GetResult(); + } + + public async Task ExecuteAsync(OnGetProjectionEvent pipelineEvent) + { + await Trace(pipelineEvent); + } + + public void Execute(OnAfterGetProjectionEvent pipelineEvent) + { + Trace(pipelineEvent).GetAwaiter().GetResult(); + } + + public async Task ExecuteAsync(OnAfterGetProjectionEvent pipelineEvent) + { + Guard.AgainstNull(pipelineEvent, nameof(pipelineEvent)); + + await Trace(pipelineEvent, $"working = {pipelineEvent.Pipeline.State.GetWorking()} / has event = {pipelineEvent.Pipeline.State.GetProjectionEvent() != null}"); + } + + public void Execute(OnGetProjectionEventEnvelope pipelineEvent) + { + Trace(pipelineEvent).GetAwaiter().GetResult(); + } + + public async Task ExecuteAsync(OnGetProjectionEventEnvelope pipelineEvent) + { + await Trace(pipelineEvent); + } + + public void Execute(OnAfterGetProjectionEventEnvelope pipelineEvent) + { + Trace(pipelineEvent).GetAwaiter().GetResult(); + } + + public async Task ExecuteAsync(OnAfterGetProjectionEventEnvelope pipelineEvent) + { + await Trace(pipelineEvent); + } + + public void Execute(OnProcessEvent pipelineEvent) + { + Trace(pipelineEvent).GetAwaiter().GetResult(); + } + + public async Task ExecuteAsync(OnProcessEvent pipelineEvent) + { + await Trace(pipelineEvent); + } + + public void Execute(OnAfterProcessEvent pipelineEvent) + { + Trace(pipelineEvent).GetAwaiter().GetResult(); + } + + public async Task ExecuteAsync(OnAfterProcessEvent pipelineEvent) + { + await Trace(pipelineEvent); + } + + public void Execute(OnAcknowledgeEvent pipelineEvent) + { + Trace(pipelineEvent).GetAwaiter().GetResult(); + } + + public async Task ExecuteAsync(OnAcknowledgeEvent pipelineEvent) + { + await Trace(pipelineEvent); + } + + public void Execute(OnAfterAcknowledgeEvent pipelineEvent) + { + Trace(pipelineEvent).GetAwaiter().GetResult(); + } + + public async Task ExecuteAsync(OnAfterAcknowledgeEvent pipelineEvent) + { + await Trace(pipelineEvent); + } + + public void Execute(OnCompleteTransactionScope pipelineEvent) + { + Trace(pipelineEvent).GetAwaiter().GetResult(); + } + + public async Task ExecuteAsync(OnCompleteTransactionScope pipelineEvent) + { + await Trace(pipelineEvent); + } + + public void Execute(OnDisposeTransactionScope pipelineEvent) + { + Trace(pipelineEvent).GetAwaiter().GetResult(); + } + + public async Task ExecuteAsync(OnDisposeTransactionScope pipelineEvent) + { + await Trace(pipelineEvent); + } + } +} \ No newline at end of file diff --git a/Shuttle.Recall.Logging/IRecallLoggingConfiguration.cs b/Shuttle.Recall.Logging/IRecallLoggingConfiguration.cs new file mode 100644 index 0000000..826d0f4 --- /dev/null +++ b/Shuttle.Recall.Logging/IRecallLoggingConfiguration.cs @@ -0,0 +1,10 @@ +using System; + +namespace Shuttle.Recall.Logging +{ + public interface IRecallLoggingConfiguration + { + bool ShouldLogPipelineType(Type pipelineType); + bool ShouldLogPipelineEventType(Type pipelineEventType); + } +} \ No newline at end of file diff --git a/Shuttle.Recall.Logging/PipelineObserver.cs b/Shuttle.Recall.Logging/PipelineObserver.cs new file mode 100644 index 0000000..dd7a1a2 --- /dev/null +++ b/Shuttle.Recall.Logging/PipelineObserver.cs @@ -0,0 +1,83 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Shuttle.Core.Contract; +using Shuttle.Core.Pipelines; +using Shuttle.Core.Reflection; + +namespace Shuttle.Recall.Logging +{ + public abstract class PipelineObserver : + IPipelineObserver, + IPipelineObserver, + IPipelineObserver + { + private readonly ILogger _logger; + private readonly IRecallLoggingConfiguration _recallLoggingConfiguration; + + private readonly Dictionary _eventCounts = new Dictionary(); + + protected PipelineObserver(ILogger logger, IRecallLoggingConfiguration recallLoggingConfiguration) + { + Guard.AgainstNull(logger, nameof(logger)); + Guard.AgainstNull(recallLoggingConfiguration, nameof(recallLoggingConfiguration)); + + _logger = logger; + _recallLoggingConfiguration = recallLoggingConfiguration; + } + + protected async Task Trace(IPipelineEvent pipelineEvent, string message = "") + { + Guard.AgainstNull(pipelineEvent, nameof(pipelineEvent)); + + var type = pipelineEvent.GetType(); + + if (!_recallLoggingConfiguration.ShouldLogPipelineEventType(type)) + { + return; + } + + if (!_eventCounts.ContainsKey(type)) + { + _eventCounts.Add(type, 0); + } + + _eventCounts[type] += 1; + + _logger.LogTrace($"[{type.Name} (thread {System.Threading.Thread.CurrentThread.ManagedThreadId}) / {_eventCounts[type]}] : pipeline = {pipelineEvent.Pipeline.GetType().FullName}{(string.IsNullOrEmpty(message) ? string.Empty : $" / {message}")}"); + + await Task.CompletedTask; + } + + public void Execute(OnAbortPipeline pipelineEvent) + { + Trace(pipelineEvent).GetAwaiter().GetResult(); + } + + public async Task ExecuteAsync(OnAbortPipeline pipelineEvent) + { + await Trace(pipelineEvent); + } + + public void Execute(OnPipelineStarting pipelineEvent) + { + Trace(pipelineEvent).GetAwaiter().GetResult(); + } + + public async Task ExecuteAsync(OnPipelineStarting pipelineEvent) + { + await Trace(pipelineEvent); + } + + public void Execute(OnPipelineException pipelineEvent) + { + ExecuteAsync(pipelineEvent).GetAwaiter().GetResult(); + } + + public async Task ExecuteAsync(OnPipelineException pipelineEvent) + { + await Trace(pipelineEvent, $"exception = '{pipelineEvent.Pipeline.Exception?.AllMessages()}'"); + } + } +} \ No newline at end of file diff --git a/Shuttle.Recall.Logging/Properties/AssemblyInfo.cs b/Shuttle.Recall.Logging/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..3db87db --- /dev/null +++ b/Shuttle.Recall.Logging/Properties/AssemblyInfo.cs @@ -0,0 +1,22 @@ +using System.Reflection; +using System.Runtime.InteropServices; + +#if NETFRAMEWORK +[assembly: AssemblyTitle(".NET Framework")] +#endif + +#if NETCOREAPP +[assembly: AssemblyTitle(".NET Core")] +#endif + +#if NETSTANDARD +[assembly: AssemblyTitle(".NET Standard")] +#endif + +[assembly: AssemblyVersion("17.0.0.0")] +[assembly: AssemblyCopyright("Copyright (c) 2024, Eben Roux")] +[assembly: AssemblyProduct("Shuttle.Recall.Logging")] +[assembly: AssemblyCompany("Eben Roux")] +[assembly: AssemblyConfiguration("Release")] +[assembly: AssemblyInformationalVersion("17.0.0")] +[assembly: ComVisible(false)] \ No newline at end of file diff --git a/Shuttle.Recall.Logging/RecallLoggingBuilder.cs b/Shuttle.Recall.Logging/RecallLoggingBuilder.cs new file mode 100644 index 0000000..bfee332 --- /dev/null +++ b/Shuttle.Recall.Logging/RecallLoggingBuilder.cs @@ -0,0 +1,26 @@ +using System; +using Microsoft.Extensions.DependencyInjection; +using Shuttle.Core.Contract; + +namespace Shuttle.Recall.Logging +{ + public class RecallLoggingBuilder + { + private RecallLoggingOptions _recallLoggingOptions = new RecallLoggingOptions(); + + public RecallLoggingOptions Options + { + get => _recallLoggingOptions; + set => _recallLoggingOptions = value ?? throw new ArgumentNullException(nameof(value)); + } + + public IServiceCollection Services { get; } + + public RecallLoggingBuilder(IServiceCollection services) + { + Guard.AgainstNull(services, nameof(services)); + + Services = services; + } + } +} \ No newline at end of file diff --git a/Shuttle.Recall.Logging/RecallLoggingConfiguration.cs b/Shuttle.Recall.Logging/RecallLoggingConfiguration.cs new file mode 100644 index 0000000..fc724b4 --- /dev/null +++ b/Shuttle.Recall.Logging/RecallLoggingConfiguration.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Shuttle.Core.Contract; + +namespace Shuttle.Recall.Logging +{ + public class RecallLoggingConfiguration : IRecallLoggingConfiguration + { + private readonly List _pipelineTypes = new List(); + private readonly List _pipelineEventTypes = new List(); + + public RecallLoggingConfiguration(IOptions recallLoggingOptions, ILogger logger) + { + Guard.AgainstNull(recallLoggingOptions, nameof(recallLoggingOptions)); + Guard.AgainstNull(recallLoggingOptions.Value, nameof(recallLoggingOptions.Value)); + Guard.AgainstNull(logger, nameof(logger)); + + foreach (var pipelineType in recallLoggingOptions.Value.PipelineTypes) + { + try + { + _pipelineTypes.Add(Type.GetType(pipelineType)); + } + catch (Exception ex) + { + logger.LogError(ex.Message); + } + } + + foreach (var pipelineEventType in recallLoggingOptions.Value.PipelineEventTypes) + { + try + { + _pipelineEventTypes.Add(Type.GetType(pipelineEventType)); + } + catch (Exception ex) + { + logger.LogError(ex.Message); + } + } + } + + public bool ShouldLogPipelineType(Type pipelineType) + { + Guard.AgainstNull(pipelineType, nameof(pipelineType)); + + return !_pipelineTypes.Any() || _pipelineTypes.Contains(pipelineType); + } + + public bool ShouldLogPipelineEventType(Type pipelineEventType) + { + Guard.AgainstNull(pipelineEventType, nameof(pipelineEventType)); + + return !_pipelineEventTypes.Any() || _pipelineEventTypes.Contains(pipelineEventType); + } + } +} \ No newline at end of file diff --git a/Shuttle.Recall.Logging/RecallLoggingOptions.cs b/Shuttle.Recall.Logging/RecallLoggingOptions.cs new file mode 100644 index 0000000..49c72ef --- /dev/null +++ b/Shuttle.Recall.Logging/RecallLoggingOptions.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; + +namespace Shuttle.Recall.Logging +{ + public class RecallLoggingOptions + { + public List PipelineEventTypes { get; set; } = new List(); + public List PipelineTypes { get; set; } = new List(); + public bool Threading { get; set; } + } +} \ No newline at end of file diff --git a/Shuttle.Recall.Logging/RecallLoggingOptionsExtensions.cs b/Shuttle.Recall.Logging/RecallLoggingOptionsExtensions.cs new file mode 100644 index 0000000..1b1b12f --- /dev/null +++ b/Shuttle.Recall.Logging/RecallLoggingOptionsExtensions.cs @@ -0,0 +1,48 @@ +using System; +using Shuttle.Core.Contract; + +namespace Shuttle.Recall.Logging +{ + public static class RecallLoggingOptionsExtensions + { + public static RecallLoggingOptions AddPipelineType( + this RecallLoggingOptions recallLoggingOptions) + { + return recallLoggingOptions.AddPipelineType(typeof(T)); + } + + public static RecallLoggingOptions AddPipelineType(this RecallLoggingOptions recallLoggingOptions, Type type) + { + Guard.AgainstNull(type, nameof(type)); + + if (recallLoggingOptions.PipelineTypes == null) + { + throw new InvalidOperationException(Resources.PipelineTypesNullException); + } + + recallLoggingOptions.PipelineTypes.Add(type.AssemblyQualifiedName); + + return recallLoggingOptions; + } + + public static RecallLoggingOptions AddPipelineEventType(this RecallLoggingOptions recallLoggingOptions) + { + return recallLoggingOptions.AddPipelineEventType(typeof(T)); + } + + public static RecallLoggingOptions AddPipelineEventType(this RecallLoggingOptions recallLoggingOptions, Type type) + { + Guard.AgainstNull(type, nameof(type)); + + if (recallLoggingOptions.PipelineEventTypes == null) + { + throw new InvalidOperationException(Resources.PipelineTypesNullException); + } + + recallLoggingOptions.PipelineEventTypes.Add(type.AssemblyQualifiedName); + + return recallLoggingOptions; + } + + } +} \ No newline at end of file diff --git a/Shuttle.Recall.Logging/Resources.Designer.cs b/Shuttle.Recall.Logging/Resources.Designer.cs new file mode 100644 index 0000000..f032ea6 --- /dev/null +++ b/Shuttle.Recall.Logging/Resources.Designer.cs @@ -0,0 +1,72 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Shuttle.Recall.Logging { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + public class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + public static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Shuttle.Recall.Logging.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + public static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to The logging options `PipelineTypes` list is `null`.. + /// + public static string PipelineTypesNullException { + get { + return ResourceManager.GetString("PipelineTypesNullException", resourceCulture); + } + } + } +} diff --git a/Shuttle.Recall.Logging/Resources.resx b/Shuttle.Recall.Logging/Resources.resx new file mode 100644 index 0000000..58fb8b3 --- /dev/null +++ b/Shuttle.Recall.Logging/Resources.resx @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + The logging options `PipelineTypes` list is `null`. + + \ No newline at end of file diff --git a/Shuttle.Recall.Logging/ServiceCollectionExtensions.cs b/Shuttle.Recall.Logging/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..1dd91d6 --- /dev/null +++ b/Shuttle.Recall.Logging/ServiceCollectionExtensions.cs @@ -0,0 +1,32 @@ +using System; +using Microsoft.Extensions.DependencyInjection; +using Shuttle.Core.Contract; + +namespace Shuttle.Recall.Logging +{ + public static class ServiceCollectionExtensions + { + public static IServiceCollection AddRecallLogging(this IServiceCollection services, Action? builder = null) + { + Guard.AgainstNull(services, nameof(services)); + + var recallLoggingBuilder = new RecallLoggingBuilder(services); + + builder?.Invoke(recallLoggingBuilder); + + services.AddOptions().Configure(options => + { + options.PipelineTypes = recallLoggingBuilder.Options.PipelineTypes; + options.PipelineEventTypes = recallLoggingBuilder.Options.PipelineEventTypes; + options.Threading = recallLoggingBuilder.Options.Threading; + }); + + services.AddHostedService(); + services.AddHostedService(); + + services.AddSingleton(); + + return services; + } + } +} diff --git a/Shuttle.Recall.Logging/Shuttle.Recall.Logging.csproj b/Shuttle.Recall.Logging/Shuttle.Recall.Logging.csproj new file mode 100644 index 0000000..218b78c --- /dev/null +++ b/Shuttle.Recall.Logging/Shuttle.Recall.Logging.csproj @@ -0,0 +1,37 @@ + + + + + netstandard2.1 + false + enable + + + + + + + + + + + + + + + + + True + True + Resources.resx + + + + + + PublicResXFileCodeGenerator + Resources.Designer.cs + + + + diff --git a/Shuttle.Recall.Logging/ThreadingLogger.cs b/Shuttle.Recall.Logging/ThreadingLogger.cs new file mode 100644 index 0000000..4b5b0c4 --- /dev/null +++ b/Shuttle.Recall.Logging/ThreadingLogger.cs @@ -0,0 +1,60 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Shuttle.Core.Contract; +using Shuttle.Core.Pipelines; + +namespace Shuttle.Recall.Logging +{ + public class ThreadingLogger : IHostedService + { + private readonly Type _pipelineType = typeof(EventProcessorStartupPipeline); + private readonly ILogger _logger; + private readonly IPipelineFactory _pipelineFactory; + private readonly RecallLoggingOptions _recallLoggingOptions; + + public ThreadingLogger(IOptions serviceBusLoggingOptions, ILogger logger, IPipelineFactory pipelineFactory) + { + Guard.AgainstNull(serviceBusLoggingOptions, nameof(serviceBusLoggingOptions)); + + _recallLoggingOptions = Guard.AgainstNull(serviceBusLoggingOptions.Value, nameof(serviceBusLoggingOptions.Value)); + _logger = Guard.AgainstNull(logger, nameof(logger)); + _pipelineFactory = Guard.AgainstNull(pipelineFactory, nameof(pipelineFactory)); + + if (_recallLoggingOptions.Threading) + { + _pipelineFactory.PipelineCreated += OnPipelineCreated; + } + } + + public async Task StartAsync(CancellationToken cancellationToken) + { + await Task.CompletedTask; + } + + private void OnPipelineCreated(object sender, PipelineEventArgs args) + { + if (args.Pipeline.GetType() != _pipelineType) + { + return; + } + + args.Pipeline.RegisterObserver(new ThreadingObserver(_logger)); + } + + public async Task StopAsync(CancellationToken cancellationToken) + { + if (_recallLoggingOptions.Threading) + { + _pipelineFactory.PipelineCreated -= OnPipelineCreated; + + + } + + await Task.CompletedTask; + } + } +} \ No newline at end of file diff --git a/Shuttle.Recall.Logging/ThreadingObserver.cs b/Shuttle.Recall.Logging/ThreadingObserver.cs new file mode 100644 index 0000000..b799515 --- /dev/null +++ b/Shuttle.Recall.Logging/ThreadingObserver.cs @@ -0,0 +1,112 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Shuttle.Core.Contract; +using Shuttle.Core.Pipelines; +using Shuttle.Core.Threading; + +namespace Shuttle.Recall.Logging +{ + public class ThreadingObserver : + IPipelineObserver, + IDisposable + { + private readonly ILogger _logger; + private readonly List _wiredProcessorThreadPools = new List(); + private readonly List _wiredProcessorThreads = new List(); + + public ThreadingObserver(ILogger logger) + { + _logger = Guard.AgainstNull(logger, nameof(logger)); + } + + public void Dispose() + { + _wiredProcessorThreads.ForEach(item => + { + item.ProcessorException -= OnProcessorException; + item.ProcessorExecuting -= OnProcessorExecuting; + item.ProcessorThreadActive -= OnProcessorThreadActive; + item.ProcessorThreadStarting -= OnProcessorThreadStarting; + item.ProcessorThreadStopped -= OnProcessorThreadStopped; + item.ProcessorThreadStopping -= OnProcessorThreadStopping; + item.ProcessorThreadOperationCanceled -= OnProcessorThreadOperationCanceled; + }); + + _wiredProcessorThreadPools.ForEach(item => item.ProcessorThreadCreated -= OnProcessorThreadCreated); + } + + public void Execute(OnAfterConfigureThreadPools pipelineEvent) + { + ExecuteAsync(pipelineEvent).GetAwaiter().GetResult(); + } + + public async Task ExecuteAsync(OnAfterConfigureThreadPools pipelineEvent) + { + Wire(pipelineEvent.Pipeline.State.Get("EventProcessorThreadPool")); + + await Task.CompletedTask; + } + + private void OnProcessorException(object sender, ProcessorThreadExceptionEventArgs e) + { + _logger.LogTrace($@"[ProcessorException] : name = '{e.Name}' / processor = {((ProcessorThread)sender).Processor.GetType().FullName} / managed thread id = {e.ManagedThreadId} / exception = '{e.Exception}'"); + } + + private void OnProcessorExecuting(object sender, ProcessorThreadEventArgs e) + { + _logger.LogTrace($@"[ProcessorExecuting] : name = '{e.Name}' / processor = {((ProcessorThread)sender).Processor.GetType().FullName} / managed thread id = {e.ManagedThreadId}"); + } + + private void OnProcessorThreadActive(object sender, ProcessorThreadEventArgs e) + { + _logger.LogTrace($@"[ProcessorThreadActive] : name = '{e.Name}' / processor = {((ProcessorThread)sender).Processor.GetType().FullName} / managed thread id = {e.ManagedThreadId}"); + } + + private void OnProcessorThreadCreated(object sender, ProcessorThreadCreatedEventArgs e) + { + _wiredProcessorThreads.Add(e.ProcessorThread); + + e.ProcessorThread.ProcessorException += OnProcessorException; + e.ProcessorThread.ProcessorExecuting += OnProcessorExecuting; + e.ProcessorThread.ProcessorThreadActive += OnProcessorThreadActive; + e.ProcessorThread.ProcessorThreadStarting += OnProcessorThreadStarting; + e.ProcessorThread.ProcessorThreadStopped += OnProcessorThreadStopped; + e.ProcessorThread.ProcessorThreadStopping += OnProcessorThreadStopping; + e.ProcessorThread.ProcessorThreadOperationCanceled += OnProcessorThreadOperationCanceled; + } + + private void OnProcessorThreadOperationCanceled(object sender, ProcessorThreadEventArgs e) + { + _logger.LogTrace($@"[ProcessorThreadOperationCanceled] : name = '{e.Name}' / processor = {((ProcessorThread)sender).Processor.GetType().FullName} / managed thread id = {e.ManagedThreadId}"); + } + + private void OnProcessorThreadStarting(object sender, ProcessorThreadEventArgs e) + { + _logger.LogTrace($@"[ProcessorThreadStarting] : name = '{e.Name}' / processor = {((ProcessorThread)sender).Processor.GetType().FullName} / managed thread id = {e.ManagedThreadId}"); + } + + private void OnProcessorThreadStopped(object sender, ProcessorThreadStoppedEventArgs e) + { + _logger.LogTrace($@"[ProcessorThreadStopped] : name = '{e.Name}' / processor = {((ProcessorThread)sender).Processor.GetType().FullName} / managed thread id = {e.ManagedThreadId} / aborted = '{e.Aborted}'"); + } + + private void OnProcessorThreadStopping(object sender, ProcessorThreadEventArgs e) + { + _logger.LogTrace($@"[ProcessorThreadStopping] : name = '{e.Name}' / processor = {((ProcessorThread)sender).Processor.GetType().FullName} / managed thread id = {e.ManagedThreadId}"); + } + + private void Wire(IProcessorThreadPool? processorThreadPool) + { + if (processorThreadPool == null) + { + return; + } + + _wiredProcessorThreadPools.Add(processorThreadPool); + + processorThreadPool.ProcessorThreadCreated += OnProcessorThreadCreated; + } + } +} \ No newline at end of file