Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions docs/guide/logging.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using Marten;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Shouldly;
using Wolverine.Marten;
using WolverineWebApi.Marten;
Expand Down Expand Up @@ -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<OrderStatus>();

await Scenario(x =>
{
x.Post.Json(new ShipOrder(status1.OrderId)).ToUrl("/orders/ship");

x.StatusCodeShouldBe(204);
});

var chain = Host.Services.GetRequiredService<WolverineHttpOptions>().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<OrderStatus>();

await Scenario(x =>
{
x.Post.Json(new ShipOrder(status1.OrderId)).ToUrl("/orders/ship3");

x.StatusCodeShouldBe(204);
});

var chain = Host.Services.GetRequiredService<WolverineHttpOptions>().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()
{
Expand Down
8 changes: 8 additions & 0 deletions src/Http/Wolverine.Http/HttpChain.Codegen.cs
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,14 @@ internal IEnumerable<Frame> 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));
Expand Down
17 changes: 17 additions & 0 deletions src/Http/Wolverine.Http/HttpChain.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<IMayInferMessageIdentity>().ToArray();

foreach (var att in atts)
{
if (att.TryInferMessageIdentity(this, out property)) return true;
}

property = default;
return false;
}

public override bool ShouldFlushOutgoingMessages()
{
return true;
Expand Down
10 changes: 10 additions & 0 deletions src/Http/WolverineWebApi/Marten/Orders.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 0 additions & 2 deletions src/Http/WolverineWebApi/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -133,8 +133,6 @@
});

opts.Policies.Add<BroadcastClientMessages>();

opts.CodeGeneration.TypeLoadMode = TypeLoadMode.Auto;
});

// These settings would apply to *both* Marten and Wolverine
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,6 @@ public async Task InitializeAsync()
.IntegrateWithWolverine();

opts.Services.AddResourceSetupOnStartup();

opts.CodeGeneration.TypeLoadMode = TypeLoadMode.Auto;
}).StartAsync();

theStore = theHost.Services.GetRequiredService<IDocumentStore>();
Expand Down Expand Up @@ -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<RaiseABC>();
var chain = theHost.GetRuntime().Handlers.ChainFor<RaiseABC>();

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()
{
Expand Down
22 changes: 20 additions & 2 deletions src/Persistence/Wolverine.Marten/AggregateHandlerAttribute.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Diagnostics;
using System.Reflection;
using JasperFx;
using JasperFx.CodeGeneration;
Expand Down Expand Up @@ -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;
}
Expand Down
2 changes: 1 addition & 1 deletion src/Persistence/Wolverine.Marten/AggregateHandling.cs
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ internal static void ValidateMethodSignatureForEmittedEvents(IChain chain, Metho
}
}
}

internal static MemberInfo DetermineAggregateIdMember(Type aggregateType, Type commandType)
{
var conventionalMemberName = $"{aggregateType.Name}Id";
Expand Down
25 changes: 20 additions & 5 deletions src/Persistence/Wolverine.Marten/WriteAggregateAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
38 changes: 36 additions & 2 deletions src/Testing/CoreTests/Persistence/Sagas/saga_action_discovery.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -33,6 +36,33 @@ public void finds_actions_on_saga_state_handler_classes()
chainFor<SagaMessage2>().ShouldNotBeNull();
}

[Fact]
public void automatic_audit_of_saga_message_saga_id()
{
// Force it to compile
var handler = Handlers.HandlerFor<SagaMessage2>();

var handlerChain = chainFor<SagaMessage2>();
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<SagaMessage1>();

var handlerChain = chainFor<SagaMessage1>();
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()
{
Expand Down Expand Up @@ -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;
3 changes: 3 additions & 0 deletions src/Wolverine/Configuration/Chain.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ public abstract class Chain<TChain, TModifyAttribute> : IChain

public abstract string Description { get; }
public List<AuditedMember> AuditedMembers { get; } = [];

public abstract bool TryInferMessageIdentity(out PropertyInfo? property);

public abstract bool ShouldFlushOutgoingMessages();
public abstract bool RequiresOutbox();

Expand Down
2 changes: 2 additions & 0 deletions src/Wolverine/Configuration/IChain.cs
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,8 @@ public interface IChain
/// </summary>
/// <param name="variable"></param>
Frame[] AddStopConditionIfNull(Variable data, Variable? identity, IDataRequirement requirement);

bool TryInferMessageIdentity(out PropertyInfo? property);
}

#endregion
3 changes: 3 additions & 0 deletions src/Wolverine/Configuration/IHandlerPolicy.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
using JasperFx;
using JasperFx.CodeGeneration;
using JasperFx.Core;
using Wolverine.Logging;
using Wolverine.Runtime;
using Wolverine.Runtime.Handlers;

Expand Down Expand Up @@ -51,3 +53,4 @@ public interface IHandlerPolicy : IWolverinePolicy
}

#endregion

14 changes: 13 additions & 1 deletion src/Wolverine/Persistence/Sagas/SagaChain.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -42,9 +43,15 @@ public SagaChain(WolverineOptions options, IGrouping<Type, HandlerCall> 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;
Expand Down Expand Up @@ -90,6 +97,11 @@ internal override List<Frame> 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);

Expand Down
Loading
Loading