diff --git a/src/Http/Wolverine.Http.Marten/AggregateAttribute.cs b/src/Http/Wolverine.Http.Marten/AggregateAttribute.cs
index a93eae6b2..2a14d1c2c 100644
--- a/src/Http/Wolverine.Http.Marten/AggregateAttribute.cs
+++ b/src/Http/Wolverine.Http.Marten/AggregateAttribute.cs
@@ -8,8 +8,7 @@
using Marten;
using Marten.Events;
using Microsoft.AspNetCore.Http;
-using Wolverine.Configuration;
-using Wolverine.Http.CodeGen;
+using Wolverine.Logging;
using Wolverine.Marten;
using Wolverine.Marten.Codegen;
using Wolverine.Marten.Publishing;
@@ -78,6 +77,10 @@ public override Variable Modify(HttpChain chain, ParameterInfo parameter, IConta
chain.Middleware.Add(new EventStoreFrame());
var loader = typeof(LoadAggregateFrame<>).CloseAndBuildAs(this, AggregateType!);
chain.Middleware.Add(loader);
+ var auditedMembersNotAlreadyPresent = AuditedMember.GetAllFromType(AggregateType).Where(member => !chain.AuditedMembers
+ .Select(m => m.MemberName)
+ .Contains(member.MemberName)).ToList();
+ chain.Middleware.Add(new LoggerBeginScopeWithAuditForAggregateFrame(chain, container, auditedMembersNotAlreadyPresent, loader.Creates.Single()));
// Use the active document session as an IQuerySession instead of creating a new one
chain.Method.TrySetArgument(new Variable(typeof(IQuerySession), sessionCreator.ReturnVariable!.Usage));
diff --git a/src/Http/Wolverine.Http/Logging/LoggingPolicies.cs b/src/Http/Wolverine.Http/Logging/LoggingPolicies.cs
new file mode 100644
index 000000000..002612025
--- /dev/null
+++ b/src/Http/Wolverine.Http/Logging/LoggingPolicies.cs
@@ -0,0 +1,39 @@
+
+using JasperFx.CodeGeneration;
+using JasperFx.CodeGeneration.Frames;
+using Lamar;
+using Wolverine.Configuration;
+using Wolverine.Logging;
+
+namespace Wolverine.Http.Logging;
+
+public class AuditLoggingPolicy : IHttpPolicy
+{
+ public void Apply(IReadOnlyList chains, GenerationRules rules, IContainer container)
+ => new Wolverine.Logging.AuditLoggingPolicy().Apply(chains, rules, container);
+}
+
+public class AddConstantsToLoggingContextPolicy : IHttpPolicy
+{
+ readonly (string, object)[] _loggingConstants;
+
+ public AddConstantsToLoggingContextPolicy(params (string, object)[] loggingConstants)
+ {
+ _loggingConstants = loggingConstants;
+ }
+ public void Apply(IReadOnlyList chains, GenerationRules rules, IContainer container)
+ => new Wolverine.Logging.AddConstantsToLoggingContextPolicy(_loggingConstants).Apply(chains, rules, container);
+}
+
+public static class WolverineHttpOptionsExtensions
+{
+ public static void AddLoggingConstantsFor(this WolverineHttpOptions options, params (string, object)[] kvp)
+ {
+ options.Policies.Add(new AddConstantsToLoggingContextPolicy(kvp));
+ }
+
+ public static void AddAuditLogging(this WolverineHttpOptions options, params (string, object)[] kvp)
+ {
+ options.Policies.Add(new AuditLoggingPolicy());
+ }
+}
diff --git a/src/Persistence/Wolverine.Marten/AggregateHandlerAttribute.cs b/src/Persistence/Wolverine.Marten/AggregateHandlerAttribute.cs
index 9a7af2804..5e0e6b5d5 100644
--- a/src/Persistence/Wolverine.Marten/AggregateHandlerAttribute.cs
+++ b/src/Persistence/Wolverine.Marten/AggregateHandlerAttribute.cs
@@ -11,6 +11,7 @@
using Marten.Schema;
using Wolverine.Attributes;
using Wolverine.Configuration;
+using Wolverine.Logging;
using Wolverine.Marten.Codegen;
using Wolverine.Marten.Publishing;
using Wolverine.Runtime.Handlers;
@@ -75,9 +76,7 @@ public override void Modify(IChain chain, GenerationRules rules, IContainer cont
AggregateType ??= DetermineAggregateType(chain);
AggregateIdMember = DetermineAggregateIdMember(AggregateType, CommandType);
VersionMember = DetermineVersionMember(CommandType);
-
-
var sessionCreator = MethodCall.For(x => x.OpenSession(null!));
chain.Middleware.Add(sessionCreator);
@@ -89,7 +88,11 @@ public override void Modify(IChain chain, GenerationRules rules, IContainer cont
chain.Middleware.Add(new MissingAggregateCheckFrame(AggregateType, CommandType, AggregateIdMember,
loader.ReturnVariable!));
}
-
+ var auditedMembersNotAlreadyPresent = AuditedMember.GetAllFromType(AggregateType)
+ .Where(member => !chain.AuditedMembers
+ .Select(m => m.MemberName)
+ .Contains(member.MemberName)).ToList();
+ chain.Middleware.Add(new LoggerBeginScopeWithAuditForAggregateFrame(chain, container, auditedMembersNotAlreadyPresent, loader.ReturnVariable!));
// Use the active document session as an IQuerySession instead of creating a new one
firstCall.TrySetArgument(new Variable(typeof(IQuerySession), sessionCreator.ReturnVariable!.Usage));
diff --git a/src/Testing/CoreTests/Compilation/handler_with_logged_audit_members.cs b/src/Testing/CoreTests/Compilation/handler_with_logged_audit_members.cs
new file mode 100644
index 000000000..e5ce9b162
--- /dev/null
+++ b/src/Testing/CoreTests/Compilation/handler_with_logged_audit_members.cs
@@ -0,0 +1,50 @@
+using System;
+using System.Threading.Tasks;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using TestingSupport;
+using Wolverine.Attributes;
+using Wolverine.Logging;
+using Wolverine.Runtime.Handlers;
+using Xunit;
+using Xunit.Abstractions;
+
+namespace CoreTests.Compilation;
+
+public class handler_with_logged_audit_members
+{
+ private readonly ITestOutputHelper _output;
+
+ public handler_with_logged_audit_members(ITestOutputHelper output)
+ {
+ _output = output;
+ }
+
+ [Fact]
+ public async Task can_compile()
+ {
+ using var host = await WolverineHost.ForAsync(o => o.AddAuditLogging());
+
+ var bus = host.Services.GetRequiredService();
+ await bus.InvokeAsync(new SomeMessage());
+
+ var graph = host.Services.GetRequiredService();
+ var chain = graph.ChainFor();
+
+ _output.WriteLine(chain.SourceCode);
+ }
+}
+
+public class SomeMessage
+{
+ [Audit]public Guid Id { get; set; } = Guid.NewGuid();
+ [Audit]public string SomeString { get; set; } = "test";
+}
+
+public class SomeMessageHandler
+{
+ public void Handle(SomeMessage itemCreated, ILogger logger)
+ {
+ logger.LogInformation("Some message id {Id}", itemCreated.Id);
+ }
+}
\ No newline at end of file
diff --git a/src/Wolverine/Configuration/Chain.cs b/src/Wolverine/Configuration/Chain.cs
index f182b4ff8..c893f263a 100644
--- a/src/Wolverine/Configuration/Chain.cs
+++ b/src/Wolverine/Configuration/Chain.cs
@@ -62,8 +62,7 @@ public IEnumerable ServiceDependencies(IContainer container, IReadOnlyList
///
public void Audit(MemberInfo member, string? heading = null)
{
- AuditedMembers.Add(new AuditedMember(member, heading ?? member.Name,
- member.Name.SplitPascalCase().Replace(" ", ".").ToLowerInvariant()));
+ AuditedMembers.Add(AuditedMember.Create(member, heading));
}
private bool isConfigureMethod(MethodInfo method)
@@ -85,20 +84,9 @@ private bool isConfigureMethod(MethodInfo method)
protected void applyAuditAttributes(Type type)
{
- foreach (var property in type.GetProperties())
+ foreach (var auditedMember in AuditedMember.GetAllFromType(type))
{
- if (property.TryGetAttribute(out var ratt))
- {
- Audit(property, ratt.Heading);
- }
- }
-
- foreach (var field in type.GetFields())
- {
- if (field.TryGetAttribute(out var ratt))
- {
- Audit(field, ratt.Heading);
- }
+ AuditedMembers.Add(auditedMember);
}
}
diff --git a/src/Wolverine/Logging/AuditedMember.cs b/src/Wolverine/Logging/AuditedMember.cs
index a13ddaea9..b38fd41bd 100644
--- a/src/Wolverine/Logging/AuditedMember.cs
+++ b/src/Wolverine/Logging/AuditedMember.cs
@@ -1,4 +1,6 @@
using System.Reflection;
+using JasperFx.Core.Reflection;
+using Wolverine.Attributes;
namespace Wolverine.Logging;
@@ -8,4 +10,28 @@ namespace Wolverine.Logging;
///
///
///
-public record AuditedMember(MemberInfo Member, string MemberName, string OpenTelemetryName);
\ No newline at end of file
+public record AuditedMember(MemberInfo Member, string MemberName, string OpenTelemetryName)
+{
+ public static IEnumerable GetAllFromType(Type type)
+ {
+ foreach (var property in type.GetProperties())
+ {
+ if (property.TryGetAttribute(out var att))
+ {
+ yield return Create(property, att.Heading);
+ }
+ }
+
+ foreach (var field in type.GetFields())
+ {
+ if (field.TryGetAttribute(out var att))
+ {
+ yield return Create(field, att.Heading);
+ }
+ }
+ }
+ public static AuditedMember Create(MemberInfo member, string? heading) => new(
+ member,
+ heading ?? member.Name,
+ member.Name.ToTelemetryFriendly());
+}
\ No newline at end of file
diff --git a/src/Wolverine/Logging/LoggerBeginScopeWithAuditFrame.cs b/src/Wolverine/Logging/LoggerBeginScopeWithAuditFrame.cs
new file mode 100644
index 000000000..b04b09e9f
--- /dev/null
+++ b/src/Wolverine/Logging/LoggerBeginScopeWithAuditFrame.cs
@@ -0,0 +1,98 @@
+using System.Diagnostics;
+using JasperFx.CodeGeneration;
+using JasperFx.CodeGeneration.Frames;
+using JasperFx.CodeGeneration.Model;
+using JasperFx.Core.Reflection;
+using Lamar;
+using Microsoft.Extensions.Logging;
+using Wolverine.Configuration;
+
+namespace Wolverine.Logging;
+
+public abstract class LoggerBeginScopeWithAuditBaseFrame : SyncFrame
+{
+ private readonly Type? _inputType;
+ private readonly List _loggers = [];
+ private readonly IEnumerable _loggerTypes;
+
+ protected LoggerBeginScopeWithAuditBaseFrame(IChain chain, IContainer container, IReadOnlyList auditedMembers, Variable? auditedVariable)
+ {
+ _loggerTypes = chain.ServiceDependencies(container, Type.EmptyTypes).Where(type => type.CanBeCastTo());
+ AuditedMembers = auditedMembers;
+ _inputType = chain.InputType()!;
+ AuditedVariable = auditedVariable;
+ }
+
+ protected Variable? AuditedVariable;
+ protected Variable? LoggingContext;
+ protected readonly IReadOnlyList AuditedMembers;
+
+ public override IEnumerable FindVariables(IMethodVariables chain)
+ {
+ foreach (var loggerType in _loggerTypes)
+ {
+ var logger = chain.FindVariable(loggerType);
+ _loggers.Add(logger);
+ yield return logger;
+ }
+ LoggingContext = chain.FindVariable(typeof(LoggingContext));
+ if (AuditedVariable is not null || _inputType is null)
+ {
+ yield break;
+ }
+ AuditedVariable = chain.FindVariable(_inputType);
+ yield return AuditedVariable;
+ }
+
+ public override void GenerateCode(GeneratedMethod method, ISourceWriter writer)
+ {
+ if (AuditedMembers.Count > 0 && _loggers.Count > 0 && AuditedVariable is not null)
+ {
+ writer.WriteComment("Adding audited members to log context");
+ writer.Write($"{LoggingContext!.Usage}.{nameof(Logging.LoggingContext.AddRange)}({string.Join(", ", AuditedMembers.Select(member => $"(\"{member.MemberName}\", {AuditedVariable!.Usage}.{member.Member.Name})"))});");
+ writer.WriteComment("Beginning logging scopes for new context");
+ foreach (var logger in _loggers)
+ {
+ writer.Write(
+ $"using var {createRandomVariable("disposable")} = {logger.Usage}.{nameof(ILogger.BeginScope)}({LoggingContext!.Usage});");
+ }
+ }
+ GenerateAdditionalCode(method, writer, _loggers);
+ Next?.GenerateCode(method, writer);
+ }
+
+ protected virtual void GenerateAdditionalCode(GeneratedMethod method, ISourceWriter writer, IEnumerable loggerVariables)
+ {
+ }
+ static string createRandomVariable(string prefix) => $"{prefix}_{Guid.NewGuid().ToString().Replace('-', '_')}";
+}
+
+public class LoggerBeginScopeWithAuditFrame : LoggerBeginScopeWithAuditBaseFrame
+{
+ public LoggerBeginScopeWithAuditFrame(IChain chain, IContainer container)
+ : base(chain, container, chain.AuditedMembers.AsReadOnly(), null)
+ {
+ }
+}
+
+public class LoggerBeginScopeWithAuditForAggregateFrame : LoggerBeginScopeWithAuditBaseFrame
+{
+ public LoggerBeginScopeWithAuditForAggregateFrame(IChain chain, IContainer container, IReadOnlyList members, Variable variable)
+ : base(chain, container, members, variable)
+ {
+ }
+
+ protected override void GenerateAdditionalCode(GeneratedMethod method, ISourceWriter writer, IEnumerable loggerVariables)
+ {
+ if (AuditedMembers.Count == 0)
+ {
+ return;
+ }
+ writer.WriteComment("Application-specific Open Telemetry auditing");
+ foreach (var member in AuditedMembers)
+ {
+ writer.WriteLine(
+ $"{typeof(Activity).FullNameInCode()}.{nameof(Activity.Current)}?.{nameof(Activity.SetTag)}(\"{member.OpenTelemetryName}\", {AuditedVariable!.Usage}.{member.Member.Name});");
+ }
+ }
+}
diff --git a/src/Wolverine/Logging/LoggingContext.cs b/src/Wolverine/Logging/LoggingContext.cs
new file mode 100644
index 000000000..10ee89e2e
--- /dev/null
+++ b/src/Wolverine/Logging/LoggingContext.cs
@@ -0,0 +1,87 @@
+using System.Diagnostics;
+using JasperFx.CodeGeneration;
+using JasperFx.CodeGeneration.Frames;
+using JasperFx.CodeGeneration.Model;
+using JasperFx.Core.Reflection;
+using Wolverine.Configuration;
+
+namespace Wolverine.Logging;
+
+public class LoggingContext : Dictionary
+{
+ public void AddRange(params (string, object)[] kvp)
+ {
+ foreach (var (key, value) in kvp)
+ {
+ TryAdd(key, value);
+ }
+ }
+}
+
+public class LoggingContextFrame : SyncFrame
+{
+ private readonly Variable _loggingContext;
+ public LoggingContextFrame()
+ {
+ _loggingContext = Create("wolverineLoggingContext_loggingTools");
+ }
+
+ public override IEnumerable FindVariables(IMethodVariables chain)
+ {
+ yield return _loggingContext;
+ }
+
+ public override void GenerateCode(GeneratedMethod method, ISourceWriter writer)
+ {
+ writer.Write($"var {_loggingContext.Usage} = new {typeof(LoggingContext).FullName}();");
+ Next?.GenerateCode(method, writer);
+ }
+}
+
+public class AddConstantsToLoggingContextFrame : SyncFrame
+{
+ private readonly (string, object)[] _loggingConstants;
+ private Variable _loggingContext;
+ public AddConstantsToLoggingContextFrame(params (string, object)[] loggingConstants)
+ {
+ if (loggingConstants.Any(x => x.Item2 is not string && !x.Item2.GetType().IsNumeric()))
+ {
+ throw new ArgumentException("One or more of the constants provided is not a string or a numeric type", nameof(loggingConstants));
+ }
+ _loggingConstants = loggingConstants;
+ }
+
+ public override IEnumerable FindVariables(IMethodVariables chain)
+ {
+ _loggingContext = chain.FindVariable(typeof(LoggingContext));
+ yield break;
+ }
+
+ public override void GenerateCode(GeneratedMethod method, ISourceWriter writer)
+ {
+ writer.Write($"{_loggingContext!.Usage}.{nameof(LoggingContext.AddRange)}({string.Join(", ", _loggingConstants.Select(kvp => $"(\"{kvp.Item1}\", {GetValue(kvp.Item2)})"))});");
+ foreach (var (key, value) in _loggingConstants)
+ {
+ writer.WriteLine(
+ $"{typeof(Activity).FullNameInCode()}.{nameof(Activity.Current)}?.{nameof(Activity.SetTag)}(\"{key.ToTelemetryFriendly()}\", {GetValue(value)});");
+ }
+ Next?.GenerateCode(method, writer);
+ return;
+
+ static string GetValue(object o) => o is string ? $"\"{o}\"" : o.ToString()!;
+ }
+}
+
+internal static class Extensions
+{
+ public static void AddMiddlewareAfterLoggingContextFrame(this IChain chain, params Frame[] frames)
+ {
+ var frameIndex = chain.Middleware.FindIndex(f => f is LoggingContextFrame);
+ if (frameIndex == -1)
+ {
+ frameIndex = 0;
+ chain.Middleware.Insert(frameIndex, new LoggingContextFrame());
+ }
+ chain.Middleware.InsertRange(frameIndex + 1, frames);
+ }
+}
\ No newline at end of file
diff --git a/src/Wolverine/Logging/LoggingPolicies.cs b/src/Wolverine/Logging/LoggingPolicies.cs
new file mode 100644
index 000000000..9279be282
--- /dev/null
+++ b/src/Wolverine/Logging/LoggingPolicies.cs
@@ -0,0 +1,54 @@
+using JasperFx.CodeGeneration;
+using JasperFx.CodeGeneration.Frames;
+using Lamar;
+using Wolverine.Configuration;
+
+namespace Wolverine.Logging;
+
+public class AuditLoggingPolicy : IChainPolicy
+{
+ public void Apply(IReadOnlyList chains, GenerationRules rules, IContainer container)
+ {
+ foreach (var chain in chains)
+ {
+ List frames = [];
+ if (!chain.AuditedMembers.Exists(member => member.MemberName.Equals(TenantIdLogContextFrame.TenantIdContextName))
+ && !chain.Middleware.Exists(frame => frame is TenantIdLogContextFrame))
+ {
+ frames.Add(new TenantIdLogContextFrame());
+ }
+ frames.Add(new LoggerBeginScopeWithAuditFrame(chain, container));
+ chain.AddMiddlewareAfterLoggingContextFrame(frames.ToArray());
+ }
+ }
+}
+
+public class AddConstantsToLoggingContextPolicy : IChainPolicy
+{
+ private readonly (string, object)[] _loggingConstants;
+
+ public AddConstantsToLoggingContextPolicy(params (string, object)[] loggingConstants)
+ {
+ _loggingConstants = loggingConstants;
+ }
+ public void Apply(IReadOnlyList chains, GenerationRules rules, IContainer container)
+ {
+ foreach (var chain in chains.Where(chain => typeof(TMessage).IsAssignableFrom(chain.InputType())))
+ {
+ chain.AddMiddlewareAfterLoggingContextFrame(new AddConstantsToLoggingContextFrame(_loggingConstants));
+ }
+ }
+}
+
+public static class WolverineOptionsExtensions
+{
+ public static void AddLoggingConstantsFor(this WolverineOptions options, params (string, object)[] kvp)
+ {
+ options.Policies.Add(new AddConstantsToLoggingContextPolicy(kvp));
+ }
+
+ public static void AddAuditLogging(this WolverineOptions options, params (string, object)[] kvp)
+ {
+ options.Policies.Add(new AuditLoggingPolicy());
+ }
+}
diff --git a/src/Wolverine/Logging/StringExtensions.cs b/src/Wolverine/Logging/StringExtensions.cs
new file mode 100644
index 000000000..a0a5b20f8
--- /dev/null
+++ b/src/Wolverine/Logging/StringExtensions.cs
@@ -0,0 +1,8 @@
+using JasperFx.Core;
+
+namespace Wolverine.Logging;
+
+internal static class StringExtensions
+{
+ internal static string ToTelemetryFriendly(this string s) => s.SplitPascalCase().Replace(' ', '.').ToLowerInvariant();
+}
diff --git a/src/Wolverine/Logging/TenantIdLogContextFrame.cs b/src/Wolverine/Logging/TenantIdLogContextFrame.cs
new file mode 100644
index 000000000..7ad92fb9e
--- /dev/null
+++ b/src/Wolverine/Logging/TenantIdLogContextFrame.cs
@@ -0,0 +1,26 @@
+using JasperFx.CodeGeneration;
+using JasperFx.CodeGeneration.Frames;
+using JasperFx.CodeGeneration.Model;
+using Wolverine.Runtime;
+
+namespace Wolverine.Logging;
+
+public class TenantIdLogContextFrame : SyncFrame
+{
+ public const string TenantIdContextName = "TenantId";
+ private Variable _messageContext = null!;
+ private Variable _loggingContext = null!;
+
+ public override IEnumerable FindVariables(IMethodVariables chain)
+ {
+ _messageContext = chain.FindVariable(typeof(MessageContext));
+ yield return _messageContext;
+ _loggingContext = chain.FindVariable(typeof(LoggingContext));
+ }
+
+ public override void GenerateCode(GeneratedMethod method, ISourceWriter writer)
+ {
+ writer.Write($"{_loggingContext.Usage}.{nameof(LoggingContext.Add)}(\"{TenantIdContextName}\", {_messageContext.Usage}.{nameof(MessageContext.TenantId)} ?? \"[NotSet]\");");
+ Next?.GenerateCode(method, writer);
+ }
+}