-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
…build-test-force] [pack-all-force] (#23) logging: add Paralax logging add Paralax CQRS logging ,logging: update scripts [build-test-force] [pack-all-force]
- Loading branch information
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
#!/bin/bash | ||
|
||
echo "Executing post-success scripts for branch $GITHUB_REF_NAME" | ||
echo "Starting build and NuGet package creation for Paralax.CQRS.Logging..." | ||
|
||
cd src/Paralax.CQRS.Logging/src | ||
|
||
echo "Restoring NuGet packages..." | ||
dotnet restore | ||
|
||
PACKAGE_VERSION="1.0.$GITHUB_RUN_NUMBER" | ||
echo "Building and packing the Paralax.CQRS.Logging library..." | ||
dotnet pack -c release /p:PackageVersion=$PACKAGE_VERSION --no-restore -o ./nupkg | ||
|
||
PACKAGE_PATH="./nupkg/Paralax.CQRS.Logging.$PACKAGE_VERSION.nupkg" | ||
|
||
if [ -f "$PACKAGE_PATH" ]; then | ||
echo "Checking if the package is already signed..." | ||
if dotnet nuget verify "$PACKAGE_PATH" | grep -q 'Package is signed'; then | ||
echo "Package is already signed, skipping signing." | ||
else | ||
echo "Signing the NuGet package..." | ||
dotnet nuget sign "$PACKAGE_PATH" \ | ||
--certificate-path "$CERTIFICATE_PATH" \ | ||
--timestamper http://timestamp.digicert.com | ||
fi | ||
|
||
echo "Uploading Paralax.CQRS.Logging package to NuGet..." | ||
dotnet nuget push "$PACKAGE_PATH" -k "$NUGET_API_KEY" \ | ||
-s https://api.nuget.org/v3/index.json --skip-duplicate | ||
echo "Package uploaded to NuGet." | ||
else | ||
echo "Error: Package $PACKAGE_PATH not found." | ||
exit 1 | ||
fi |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
#!/bin/bash | ||
|
||
echo "Running tests and collecting coverage for Paralax.HTTP..." | ||
|
||
cd src/Paralax.HTTP/src | ||
|
||
echo "Restoring NuGet packages..." | ||
dotnet restore | ||
|
||
echo "Running tests and generating code coverage report..." | ||
dotnet test --collect:"XPlat Code Coverage" --results-directory ./TestResults | ||
|
||
# Check if tests succeeded | ||
if [ $? -ne 0 ]; then | ||
echo "Tests failed. Exiting..." | ||
exit 1 | ||
fi | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
using System; | ||
using System.Threading; | ||
using System.Threading.Tasks; | ||
using Microsoft.Extensions.DependencyInjection; | ||
using Microsoft.Extensions.Logging; | ||
using Paralax.Core; | ||
using Paralax.CQRS.Commands; | ||
using SmartFormat; | ||
|
||
namespace Paralax.CQRS.Logging.Decorators | ||
{ | ||
[Decorator] | ||
internal sealed class CommandHandlerLoggingDecorator<TCommand> : ICommandHandler<TCommand> | ||
where TCommand : class, ICommand | ||
{ | ||
private readonly ICommandHandler<TCommand> _handler; | ||
private readonly ILogger<CommandHandlerLoggingDecorator<TCommand>> _logger; | ||
private readonly IMessageToLogTemplateMapper _mapper; | ||
|
||
public CommandHandlerLoggingDecorator(ICommandHandler<TCommand> handler, | ||
ILogger<CommandHandlerLoggingDecorator<TCommand>> logger, IServiceProvider serviceProvider) | ||
{ | ||
_handler = handler; | ||
_logger = logger; | ||
_mapper = serviceProvider.GetService<IMessageToLogTemplateMapper>() ?? new EmptyMessageToLogTemplateMapper(); | ||
} | ||
|
||
public async Task HandleAsync(TCommand command, CancellationToken cancellationToken = default) | ||
{ | ||
var template = _mapper.Map(command); | ||
|
||
if (template is null) | ||
{ | ||
await _handler.HandleAsync(command, cancellationToken); | ||
return; | ||
} | ||
|
||
try | ||
{ | ||
Log(command, template.Before); | ||
Check warning on line 40 in src/Paralax.CQRS.Logging/src/Decorators/CommandHandlerLoggingDecorator.cs GitHub Actions / publish
|
||
await _handler.HandleAsync(command, cancellationToken); | ||
Log(command, template.After); | ||
Check warning on line 42 in src/Paralax.CQRS.Logging/src/Decorators/CommandHandlerLoggingDecorator.cs GitHub Actions / publish
|
||
} | ||
catch (Exception ex) | ||
{ | ||
var exceptionTemplate = template.GetExceptionTemplate(ex); | ||
Log(command, exceptionTemplate, isError: true); | ||
Check warning on line 47 in src/Paralax.CQRS.Logging/src/Decorators/CommandHandlerLoggingDecorator.cs GitHub Actions / publish
|
||
throw; | ||
} | ||
} | ||
|
||
private void Log(TCommand command, string message, bool isError = false) | ||
{ | ||
if (string.IsNullOrEmpty(message)) | ||
{ | ||
return; | ||
} | ||
|
||
var formattedMessage = Smart.Format(message, command); | ||
|
||
if (isError) | ||
{ | ||
_logger.LogError(formattedMessage); | ||
} | ||
else | ||
{ | ||
_logger.LogInformation(formattedMessage); | ||
} | ||
} | ||
|
||
private class EmptyMessageToLogTemplateMapper : IMessageToLogTemplateMapper | ||
{ | ||
public HandlerLogTemplate Map<TMessage>(TMessage message) where TMessage : class => null; | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
using System; | ||
using System.Threading; | ||
using System.Threading.Tasks; | ||
using Microsoft.Extensions.DependencyInjection; | ||
using Microsoft.Extensions.Logging; | ||
using Paralax.Core; | ||
using Paralax.CQRS.Events; | ||
using SmartFormat; | ||
|
||
namespace Paralax.CQRS.Logging.Decorators | ||
{ | ||
[Decorator] | ||
internal sealed class EventHandlerLoggingDecorator<TEvent> : IEventHandler<TEvent> | ||
where TEvent : class, IEvent | ||
{ | ||
private readonly IEventHandler<TEvent> _handler; | ||
private readonly ILogger<EventHandlerLoggingDecorator<TEvent>> _logger; | ||
private readonly IMessageToLogTemplateMapper _mapper; | ||
|
||
public EventHandlerLoggingDecorator(IEventHandler<TEvent> handler, | ||
ILogger<EventHandlerLoggingDecorator<TEvent>> logger, IServiceProvider serviceProvider) | ||
{ | ||
_handler = handler; | ||
_logger = logger; | ||
_mapper = serviceProvider.GetService<IMessageToLogTemplateMapper>() ?? new EmptyMessageToLogTemplateMapper(); | ||
} | ||
|
||
public async Task HandleAsync(TEvent @event, CancellationToken cancellationToken = default) | ||
{ | ||
var template = _mapper.Map(@event); | ||
|
||
if (template is null) | ||
{ | ||
await _handler.HandleAsync(@event, cancellationToken); | ||
return; | ||
} | ||
|
||
try | ||
{ | ||
Log(@event, template.Before); | ||
Check warning on line 40 in src/Paralax.CQRS.Logging/src/Decorators/EventHandlerLoggingDecorator.cs GitHub Actions / publish
|
||
await _handler.HandleAsync(@event, cancellationToken); | ||
Log(@event, template.After); | ||
Check warning on line 42 in src/Paralax.CQRS.Logging/src/Decorators/EventHandlerLoggingDecorator.cs GitHub Actions / publish
|
||
} | ||
catch (Exception ex) | ||
{ | ||
var exceptionTemplate = template.GetExceptionTemplate(ex); | ||
Log(@event, exceptionTemplate, isError: true); | ||
throw; | ||
} | ||
} | ||
|
||
private void Log(TEvent @event, string message, bool isError = false) | ||
{ | ||
if (string.IsNullOrEmpty(message)) | ||
{ | ||
return; | ||
} | ||
|
||
var formattedMessage = Smart.Format(message, @event); | ||
|
||
if (isError) | ||
{ | ||
_logger.LogError(formattedMessage); | ||
} | ||
else | ||
{ | ||
_logger.LogInformation(formattedMessage); | ||
} | ||
} | ||
|
||
private class EmptyMessageToLogTemplateMapper : IMessageToLogTemplateMapper | ||
{ | ||
public HandlerLogTemplate Map<TMessage>(TMessage message) where TMessage : class => null; | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
using System; | ||
using System.Collections.Generic; | ||
using System.Linq; | ||
using System.Reflection; | ||
using System.Runtime.CompilerServices; | ||
using Microsoft.Extensions.DependencyInjection; | ||
using Scrutor; | ||
using Paralax.CQRS.Commands; | ||
using Paralax.CQRS.Events; | ||
using Paralax.CQRS.Logging.Decorators; | ||
|
||
namespace Paralax.CQRS.Logging | ||
{ | ||
public static class Extensions | ||
{ | ||
/// <summary> | ||
/// Adds logging decorators for all command handlers in the specified assembly. | ||
/// </summary> | ||
/// <param name="builder">The <see cref="IParalaxBuilder"/>.</param> | ||
/// <param name="assembly">The assembly to scan for command handlers.</param> | ||
/// <returns>The updated <see cref="IParalaxBuilder"/>.</returns> | ||
public static IParalaxBuilder AddCommandHandlersLogging(this IParalaxBuilder builder, Assembly assembly = null) | ||
=> builder.AddHandlerLogging(typeof(ICommandHandler<>), typeof(CommandHandlerLoggingDecorator<>), assembly); | ||
|
||
/// <summary> | ||
/// Adds logging decorators for all event handlers in the specified assembly. | ||
/// </summary> | ||
/// <param name="builder">The <see cref="IParalaxBuilder"/>.</param> | ||
/// <param name="assembly">The assembly to scan for event handlers.</param> | ||
/// <returns>The updated <see cref="IParalaxBuilder"/>.</returns> | ||
public static IParalaxBuilder AddEventHandlersLogging(this IParalaxBuilder builder, Assembly assembly = null) | ||
=> builder.AddHandlerLogging(typeof(IEventHandler<>), typeof(EventHandlerLoggingDecorator<>), assembly); | ||
|
||
/// <summary> | ||
/// Generic method to add logging decorators for either command or event handlers. | ||
/// </summary> | ||
private static IParalaxBuilder AddHandlerLogging(this IParalaxBuilder builder, Type handlerType, | ||
Type decoratorType, Assembly assembly = null) | ||
{ | ||
assembly ??= Assembly.GetCallingAssembly(); | ||
|
||
var handlers = assembly | ||
.GetTypes() | ||
.Where(t => t.GetInterfaces().Any(i => i.IsGenericType && i.GetGenericTypeDefinition() == handlerType)) | ||
.ToList(); | ||
|
||
handlers.ForEach(handler => | ||
{ | ||
// Find the TryDecorate method and invoke it on the appropriate service | ||
var extensionMethods = GetExtensionMethods(); | ||
var tryDecorateMethod = extensionMethods.FirstOrDefault(mi => mi.Name == "TryDecorate" && !mi.IsGenericMethod); | ||
|
||
tryDecorateMethod?.Invoke(builder.Services, new object[] | ||
{ | ||
builder.Services, | ||
handler.GetInterfaces().FirstOrDefault(), | ||
decoratorType.MakeGenericType(handler.GetInterfaces().FirstOrDefault()?.GenericTypeArguments.First()) | ||
}); | ||
}); | ||
|
||
return builder; | ||
} | ||
|
||
/// <summary> | ||
/// Retrieves the extension methods for service collection. | ||
/// </summary> | ||
private static IEnumerable<MethodInfo> GetExtensionMethods() | ||
{ | ||
var types = typeof(ReplacementBehavior).Assembly.GetTypes(); | ||
|
||
var query = from type in types | ||
where type.IsSealed && !type.IsGenericType && !type.IsNested | ||
from method in type.GetMethods(BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic) | ||
where method.IsDefined(typeof(ExtensionAttribute), false) | ||
where method.GetParameters()[0].ParameterType == typeof(IServiceCollection) | ||
select method; | ||
|
||
return query; | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
using System; | ||
using System.Collections.Generic; | ||
using System.Text.Json; | ||
|
||
namespace Paralax.CQRS.Logging | ||
{ | ||
public sealed class HandlerLogTemplate | ||
{ | ||
public string? Before { get; set; } | ||
public string? After { get; set; } | ||
public IReadOnlyDictionary<Type, string>? OnError { get; set; } | ||
|
||
public string? GetExceptionTemplate(Exception ex) | ||
{ | ||
var exceptionType = ex.GetType(); | ||
if (OnError == null) | ||
{ | ||
return null; | ||
} | ||
|
||
return OnError.TryGetValue(exceptionType, out var template) | ||
? template | ||
: "An unexpected error occurred."; | ||
} | ||
|
||
public string GetBeforeTemplate<TMessage>(TMessage message) | ||
{ | ||
var messageType = message?.GetType().Name ?? "UnknownMessage"; | ||
return Before != null | ||
? SmartFormat(Before, message) | ||
: $"Starting to handle message of type {messageType}."; | ||
} | ||
|
||
public string GetAfterTemplate<TMessage>(TMessage message) | ||
{ | ||
var messageType = message?.GetType().Name ?? "UnknownMessage"; | ||
return After != null | ||
? SmartFormat(After, message) | ||
: $"Completed handling message of type {messageType}."; | ||
} | ||
|
||
private string SmartFormat<TMessage>(string template, TMessage message) | ||
{ | ||
// Serialize anonymous types or complex objects as JSON to make them human-readable | ||
return string.Format(template, JsonSerializer.Serialize(message)); | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
namespace Paralax.CQRS.Logging | ||
{ | ||
public interface IMessageToLogTemplateMapper | ||
{ | ||
HandlerLogTemplate Map<TMessage>(TMessage message) where TMessage : class; | ||
} | ||
} | ||
|