diff --git a/docs/guide/logging.md b/docs/guide/logging.md index 6d921b2ec..0c25b2629 100644 --- a/docs/guide/logging.md +++ b/docs/guide/logging.md @@ -11,10 +11,6 @@ to selectively filter logging levels in your application, rely on the message ty ## Configuring Message Logging Levels -::: tip -This functionality was added in Wolverine 1.7. -::: - Wolverine automatically logs the execution start and stop of all message handling with `LogLevel.Debug`. Likewise, Wolverine logs the successful completion of all messages (including the capture of cascading messages and all middleware) with `LogLevel.Information`. However, many folks have found this logging to be too intrusive. Not to worry, you can quickly override the log levels @@ -139,6 +135,11 @@ for better searching within your logs. ## Contextual Logging with Audited Members +::: tip +As of verion 5.5, Wolverine will automatically audit any property that refers to a [saga identity](/guide/durability/sagas) or to an event stream +identity within the [aggregate handler workflow](/guide/durability/marten/event-sourcing) with Marten event sourcing. +::: + ::: warning Be cognizant of the information you're writing to log files or Open Telemetry data and whether or not that data is some kind of protected data like personal data identifiers. diff --git a/src/Http/Wolverine.Http.Tests/Marten/using_aggregate_handler_workflow.cs b/src/Http/Wolverine.Http.Tests/Marten/using_aggregate_handler_workflow.cs index 249dc9cc9..4429c9bde 100644 --- a/src/Http/Wolverine.Http.Tests/Marten/using_aggregate_handler_workflow.cs +++ b/src/Http/Wolverine.Http.Tests/Marten/using_aggregate_handler_workflow.cs @@ -1,5 +1,6 @@ using Marten; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; using Shouldly; using Wolverine.Marten; using WolverineWebApi.Marten; @@ -34,6 +35,54 @@ await Scenario(x => order.Items["Socks"].Ready.ShouldBeTrue(); } + [Fact] + public async Task automatically_apply_stream_id_as_audit_member_marked_with_AggregateHandler() + { + // Guaranteeing that it's warmed up + var result1 = await Scenario(x => + { + x.Post.Json(new StartOrder(["Socks", "Shoes", "Shirt"])).ToUrl("/orders/create"); + }); + + var status1 = result1.ReadAsJson(); + + await Scenario(x => + { + x.Post.Json(new ShipOrder(status1.OrderId)).ToUrl("/orders/ship"); + + x.StatusCodeShouldBe(204); + }); + + var chain = Host.Services.GetRequiredService().Endpoints!.ChainFor("POST", "/orders/ship"); + chain.ShouldNotBeNull(); + + chain.AuditedMembers.Single().MemberName.ShouldBe(nameof(ShipOrder.OrderId)); + } + + [Fact] + public async Task automatically_apply_stream_id_as_audit_member_marked_with_WriteAggregate() + { + // Guaranteeing that it's warmed up + var result1 = await Scenario(x => + { + x.Post.Json(new StartOrder(["Socks", "Shoes", "Shirt"])).ToUrl("/orders/create"); + }); + + var status1 = result1.ReadAsJson(); + + await Scenario(x => + { + x.Post.Json(new ShipOrder(status1.OrderId)).ToUrl("/orders/ship3"); + + x.StatusCodeShouldBe(204); + }); + + var chain = Host.Services.GetRequiredService().Endpoints!.ChainFor("POST", "/orders/ship3"); + chain.ShouldNotBeNull(); + + chain.AuditedMembers.Single().MemberName.ShouldBe(nameof(ShipOrder.OrderId)); + } + [Fact] public async Task mix_creation_response_and_start_stream() { diff --git a/src/Http/Wolverine.Http/HttpChain.Codegen.cs b/src/Http/Wolverine.Http/HttpChain.Codegen.cs index ac61ca647..5942eb6ee 100644 --- a/src/Http/Wolverine.Http/HttpChain.Codegen.cs +++ b/src/Http/Wolverine.Http/HttpChain.Codegen.cs @@ -115,6 +115,14 @@ internal IEnumerable DetermineFrames(GenerationRules rules) Postprocessors.Add(new WriteEmptyBodyStatusCode()); } + if (TryInferMessageIdentity(out var identity)) + { + if (AuditedMembers.All(x => x.Member != identity)) + { + Audit(identity); + } + } + if (AuditedMembers.Count != 0) { Middleware.Insert(0, new AuditToActivityFrame(this)); diff --git a/src/Http/Wolverine.Http/HttpChain.cs b/src/Http/Wolverine.Http/HttpChain.cs index 52857bad4..d2e29e508 100644 --- a/src/Http/Wolverine.Http/HttpChain.cs +++ b/src/Http/Wolverine.Http/HttpChain.cs @@ -23,6 +23,7 @@ using Wolverine.Http.Policies; using Wolverine.Persistence; using Wolverine.Runtime; +using Wolverine.Runtime.Partitioning; using ServiceContainer = JasperFx.ServiceContainer; namespace Wolverine.Http; @@ -265,6 +266,22 @@ public bool HasResourceType() return ResourceType != null && ResourceType != typeof(void) && ResourceType.FullName != "Microsoft.FSharp.Core.Unit"; } + public override bool TryInferMessageIdentity(out PropertyInfo? property) + { + var atts = Method.HandlerType.GetCustomAttributes() + .Concat(Method.Method.GetCustomAttributes()) + .Concat(Method.Method.GetParameters().SelectMany(x => x.GetCustomAttributes())) + .OfType().ToArray(); + + foreach (var att in atts) + { + if (att.TryInferMessageIdentity(this, out property)) return true; + } + + property = default; + return false; + } + public override bool ShouldFlushOutgoingMessages() { return true; diff --git a/src/Http/WolverineWebApi/Marten/Orders.cs b/src/Http/WolverineWebApi/Marten/Orders.cs index 3cd60d712..1f133586f 100644 --- a/src/Http/WolverineWebApi/Marten/Orders.cs +++ b/src/Http/WolverineWebApi/Marten/Orders.cs @@ -133,6 +133,16 @@ public static OrderShipped Ship(ShipOrder command, Order order) } #endregion + + [WolverinePost("/orders/ship3"), EmptyResponse] + // The OrderShipped return value is treated as an event being posted + // to a Marten even stream + // instead of as the HTTP response body because of the presence of + // the [EmptyResponse] attribute + public static OrderShipped Ship3(ShipOrder command, [WriteAggregate] Order order) + { + return new OrderShipped(); + } #region sample_using_aggregate_attribute_1 diff --git a/src/Http/WolverineWebApi/Program.cs b/src/Http/WolverineWebApi/Program.cs index 8e2bbea95..2ea74c15b 100644 --- a/src/Http/WolverineWebApi/Program.cs +++ b/src/Http/WolverineWebApi/Program.cs @@ -133,8 +133,6 @@ }); opts.Policies.Add(); - - opts.CodeGeneration.TypeLoadMode = TypeLoadMode.Auto; }); // These settings would apply to *both* Marten and Wolverine diff --git a/src/Persistence/MartenTests/AggregateHandlerWorkflow/aggregate_handler_workflow.cs b/src/Persistence/MartenTests/AggregateHandlerWorkflow/aggregate_handler_workflow.cs index 3e69682e2..a496c47af 100644 --- a/src/Persistence/MartenTests/AggregateHandlerWorkflow/aggregate_handler_workflow.cs +++ b/src/Persistence/MartenTests/AggregateHandlerWorkflow/aggregate_handler_workflow.cs @@ -38,8 +38,6 @@ public async Task InitializeAsync() .IntegrateWithWolverine(); opts.Services.AddResourceSetupOnStartup(); - - opts.CodeGeneration.TypeLoadMode = TypeLoadMode.Auto; }).StartAsync(); theStore = theHost.Services.GetRequiredService(); @@ -89,6 +87,18 @@ await OnAggregate(a => }); } + [Fact] + public void automatically_adding_stream_id_to_the_audit_members() + { + // Do this first to force the compilation + var handler = theHost.GetRuntime().Handlers.HandlerFor(); + var chain = theHost.GetRuntime().Handlers.ChainFor(); + + chain.AuditedMembers.Single().MemberName.ShouldBe(nameof(RaiseABC.LetterAggregateId)); + + chain.SourceCode.ShouldContain("System.Diagnostics.Activity.Current?.SetTag(\"letter.aggregate.id\", raiseABC.LetterAggregateId);"); + } + [Fact] public async Task events_then_response_invoke_with_return() { diff --git a/src/Persistence/Wolverine.Marten/AggregateHandlerAttribute.cs b/src/Persistence/Wolverine.Marten/AggregateHandlerAttribute.cs index c234da515..dcbe61eb4 100644 --- a/src/Persistence/Wolverine.Marten/AggregateHandlerAttribute.cs +++ b/src/Persistence/Wolverine.Marten/AggregateHandlerAttribute.cs @@ -1,3 +1,4 @@ +using System.Diagnostics; using System.Reflection; using JasperFx; using JasperFx.CodeGeneration; @@ -89,10 +90,27 @@ public override void Modify(IChain chain, GenerationRules rules, IServiceContain handling.Apply(chain, container); } - public bool TryInferMessageIdentity(HandlerChain chain, out PropertyInfo property) + public bool TryInferMessageIdentity(IChain chain, out PropertyInfo property) { + var inputType = chain.InputType(); + property = default!; + + // This is gross + if (inputType.Closes(typeof(IEvent<>))) + { + if (AggregateHandling.TryLoad(chain, out var handling)) + { + property = handling.AggregateId.VariableType == typeof(string) + ? inputType.GetProperty(nameof(IEvent.StreamKey)) + : inputType.GetProperty(nameof(IEvent.StreamId)); + + } + + return property != null; + } + var aggregateType = AggregateHandling.DetermineAggregateType(chain); - var idMember = AggregateHandling.DetermineAggregateIdMember(aggregateType, chain.MessageType); + var idMember = AggregateHandling.DetermineAggregateIdMember(aggregateType, inputType); property = idMember as PropertyInfo; return property != null; } diff --git a/src/Persistence/Wolverine.Marten/AggregateHandling.cs b/src/Persistence/Wolverine.Marten/AggregateHandling.cs index 1c25410af..85ce56cf7 100644 --- a/src/Persistence/Wolverine.Marten/AggregateHandling.cs +++ b/src/Persistence/Wolverine.Marten/AggregateHandling.cs @@ -128,7 +128,7 @@ internal static void ValidateMethodSignatureForEmittedEvents(IChain chain, Metho } } } - + internal static MemberInfo DetermineAggregateIdMember(Type aggregateType, Type commandType) { var conventionalMemberName = $"{aggregateType.Name}Id"; diff --git a/src/Persistence/Wolverine.Marten/WriteAggregateAttribute.cs b/src/Persistence/Wolverine.Marten/WriteAggregateAttribute.cs index 981acf2f0..a7c8e8eb7 100644 --- a/src/Persistence/Wolverine.Marten/WriteAggregateAttribute.cs +++ b/src/Persistence/Wolverine.Marten/WriteAggregateAttribute.cs @@ -133,11 +133,26 @@ public override Variable Modify(IChain chain, ParameterInfo parameter, IServiceC return null; } - public bool TryInferMessageIdentity(HandlerChain chain, out PropertyInfo property) + public bool TryInferMessageIdentity(IChain chain, out PropertyInfo property) { - var aggregateType = AggregateHandling.DetermineAggregateType(chain); - var idMember = AggregateHandling.DetermineAggregateIdMember(aggregateType, chain.MessageType); - property = idMember as PropertyInfo; - return property != null; + var inputType = chain.InputType(); + if (inputType == null) + { + property = default; + return false; + } + + // NOT PROUD OF THIS CODE! + if (AggregateHandling.TryLoad(chain, out var handling)) + { + if (handling.AggregateId is MemberAccessVariable mav) + { + property = mav.Member as PropertyInfo; + return property != null; + } + } + + property = null; + return false; } } \ No newline at end of file diff --git a/src/Testing/CoreTests/Persistence/Sagas/saga_action_discovery.cs b/src/Testing/CoreTests/Persistence/Sagas/saga_action_discovery.cs index e599344b3..16322c86c 100644 --- a/src/Testing/CoreTests/Persistence/Sagas/saga_action_discovery.cs +++ b/src/Testing/CoreTests/Persistence/Sagas/saga_action_discovery.cs @@ -1,4 +1,7 @@ -using Wolverine.ComplianceTests.Compliance; +using JasperFx.CodeGeneration; +using JasperFx.Core.Reflection; +using Wolverine.Attributes; +using Wolverine.ComplianceTests.Compliance; using Wolverine.Runtime.Handlers; using Xunit; using Xunit.Abstractions; @@ -33,6 +36,33 @@ public void finds_actions_on_saga_state_handler_classes() chainFor().ShouldNotBeNull(); } + [Fact] + public void automatic_audit_of_saga_message_saga_id() + { + // Force it to compile + var handler = Handlers.HandlerFor(); + + var handlerChain = chainFor(); + handlerChain.SourceCode.ShouldContain("System.Diagnostics.Activity.Current?.SetTag(\"Id\", sagaMessage2.Id);"); + + handlerChain.AuditedMembers.Single().MemberName + .ShouldBe(nameof(SagaMessage2.Id)); + } + + [Fact] + public void automatic_audit_of_saga_message_saga_id_with_override() + { + // Force it to compile + var handler = Handlers.HandlerFor(); + + var handlerChain = chainFor(); + handlerChain.SourceCode.ShouldContain("System.Diagnostics.Activity.Current?.SetTag(\"id\", sagaMessage1.Id);"); + + handlerChain.AuditedMembers.Single().MemberName + .ShouldBe("StreamId"); + + } + [Fact] public void finds_actions_on_saga_state_orchestrates_methods() { @@ -78,6 +108,10 @@ public void Start(SagaStarter starter) public class SagaStarter : Message3; -public class SagaMessage1 : Message1; +public class SagaMessage1 +{ + [Audit("StreamId")] + public Guid Id { get; set; } = Guid.NewGuid(); +} public class SagaMessage2 : Message2; diff --git a/src/Wolverine/Configuration/Chain.cs b/src/Wolverine/Configuration/Chain.cs index f214be433..9f44045e8 100644 --- a/src/Wolverine/Configuration/Chain.cs +++ b/src/Wolverine/Configuration/Chain.cs @@ -42,6 +42,9 @@ public abstract class Chain : IChain public abstract string Description { get; } public List AuditedMembers { get; } = []; + + public abstract bool TryInferMessageIdentity(out PropertyInfo? property); + public abstract bool ShouldFlushOutgoingMessages(); public abstract bool RequiresOutbox(); diff --git a/src/Wolverine/Configuration/IChain.cs b/src/Wolverine/Configuration/IChain.cs index 133ff99f6..282362d8d 100644 --- a/src/Wolverine/Configuration/IChain.cs +++ b/src/Wolverine/Configuration/IChain.cs @@ -163,6 +163,8 @@ public interface IChain /// /// Frame[] AddStopConditionIfNull(Variable data, Variable? identity, IDataRequirement requirement); + + bool TryInferMessageIdentity(out PropertyInfo? property); } #endregion \ No newline at end of file diff --git a/src/Wolverine/Configuration/IHandlerPolicy.cs b/src/Wolverine/Configuration/IHandlerPolicy.cs index 81747fa4a..84bdfa37f 100644 --- a/src/Wolverine/Configuration/IHandlerPolicy.cs +++ b/src/Wolverine/Configuration/IHandlerPolicy.cs @@ -1,5 +1,7 @@ using JasperFx; using JasperFx.CodeGeneration; +using JasperFx.Core; +using Wolverine.Logging; using Wolverine.Runtime; using Wolverine.Runtime.Handlers; @@ -51,3 +53,4 @@ public interface IHandlerPolicy : IWolverinePolicy } #endregion + diff --git a/src/Wolverine/Persistence/Sagas/SagaChain.cs b/src/Wolverine/Persistence/Sagas/SagaChain.cs index c4594df46..4e03fd3e2 100644 --- a/src/Wolverine/Persistence/Sagas/SagaChain.cs +++ b/src/Wolverine/Persistence/Sagas/SagaChain.cs @@ -5,6 +5,7 @@ using JasperFx.CodeGeneration.Model; using JasperFx.Core; using JasperFx.Core.Reflection; +using Wolverine.Logging; using Wolverine.Runtime; using Wolverine.Runtime.Handlers; @@ -42,9 +43,15 @@ public SagaChain(WolverineOptions options, IGrouping grouping } SagaIdMember = DetermineSagaIdMember(MessageType, SagaType); + + // Automatically audit the saga id + if (SagaIdMember != null && AuditedMembers.All(x => x.Member != SagaIdMember)) + { + AuditedMembers.Add(new AuditedMember(SagaIdMember, SagaIdMember.Name, SagaIdMember.Name)); + } } - internal override bool TryInferMessageIdentity(out PropertyInfo? property) + public override bool TryInferMessageIdentity(out PropertyInfo? property) { property = SagaIdMember as PropertyInfo; return property != null; @@ -90,6 +97,11 @@ internal override List DetermineFrames(GenerationRules rules, IServiceCon MessageVariable messageVariable) { applyCustomizations(rules, container); + + if (AuditedMembers.Count != 0) + { + Middleware.Insert(0, new AuditToActivityFrame(this)); + } var frameProvider = rules.GetPersistenceProviders(this, container); diff --git a/src/Wolverine/Runtime/Handlers/HandlerChain.cs b/src/Wolverine/Runtime/Handlers/HandlerChain.cs index 509e57726..c6ad5e531 100644 --- a/src/Wolverine/Runtime/Handlers/HandlerChain.cs +++ b/src/Wolverine/Runtime/Handlers/HandlerChain.cs @@ -236,9 +236,10 @@ void ICodeFile.AssembleTypes(GeneratedAssembly assembly) handleMethod.DerivedVariables.Add(envelopeVariable); } - internal virtual bool TryInferMessageIdentity(out PropertyInfo? property) + public override bool TryInferMessageIdentity(out PropertyInfo? property) { - var atts = Handlers.SelectMany(x => x.HandlerType.GetCustomAttributes().Concat(x.Method.GetCustomAttributes())) + var atts = Handlers + .SelectMany(x => x.HandlerType.GetCustomAttributes().Concat(x.Method.GetCustomAttributes().Concat(x.Method.GetParameters().SelectMany(p => p.GetCustomAttributes())))) .OfType().ToArray(); foreach (var att in atts) @@ -491,15 +492,25 @@ internal virtual List DetermineFrames(GenerationRules rules, IServiceCont MessageType.FullName); } + Middleware.Insert(0, messageVariable.Creator!); + + applyCustomizations(rules, container); + + // This has to be done *after* the customizations so the aggregate handler + // workflow can be applied + if (TryInferMessageIdentity(out var identity)) + { + if (AuditedMembers.All(x => x.Member != identity)) + { + Audit(identity); + } + } + if (AuditedMembers.Count != 0) { Middleware.Insert(0, new AuditToActivityFrame(this)); } - Middleware.Insert(0, messageVariable.Creator!); - - applyCustomizations(rules, container); - var handlerReturnValueFrames = determineHandlerReturnValueFrames().ToArray(); // Allow for immutable message types that get overwritten by middleware diff --git a/src/Wolverine/Runtime/Partitioning/IMayInferMessageIdentity.cs b/src/Wolverine/Runtime/Partitioning/IMayInferMessageIdentity.cs index 95446c533..2a490e3c4 100644 --- a/src/Wolverine/Runtime/Partitioning/IMayInferMessageIdentity.cs +++ b/src/Wolverine/Runtime/Partitioning/IMayInferMessageIdentity.cs @@ -1,9 +1,10 @@ using System.Reflection; +using Wolverine.Configuration; using Wolverine.Runtime.Handlers; namespace Wolverine.Runtime.Partitioning; public interface IMayInferMessageIdentity { - bool TryInferMessageIdentity(HandlerChain chain, out PropertyInfo property); + bool TryInferMessageIdentity(IChain chain, out PropertyInfo property); } \ No newline at end of file diff --git a/src/Wolverine/Wolverine.csproj b/src/Wolverine/Wolverine.csproj index 34aa4a355..4f8caa378 100644 --- a/src/Wolverine/Wolverine.csproj +++ b/src/Wolverine/Wolverine.csproj @@ -4,7 +4,7 @@ WolverineFx - +