From 333977a486277d81096365d1eff3c4ea527bdf8d Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Tue, 24 Feb 2026 12:09:19 -0600 Subject: [PATCH 1/2] Fixed replay safe logger not creating correct type Signed-off-by: Whit Waldo --- .../Worker/Internal/WorkflowOrchestrationContext.cs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/Dapr.Workflow/Worker/Internal/WorkflowOrchestrationContext.cs b/src/Dapr.Workflow/Worker/Internal/WorkflowOrchestrationContext.cs index 60614fe2a..71beff3a0 100644 --- a/src/Dapr.Workflow/Worker/Internal/WorkflowOrchestrationContext.cs +++ b/src/Dapr.Workflow/Worker/Internal/WorkflowOrchestrationContext.cs @@ -47,6 +47,7 @@ internal sealed class WorkflowOrchestrationContext : WorkflowContext private readonly SortedDictionary _pendingActions = []; private readonly IWorkflowSerializer _workflowSerializer; private readonly ILogger _logger; + private readonly ILoggerFactory _loggerFactory; // Maps runtime sub-orchestration created EventId -> parent action/task id (our local task id). private readonly Dictionary _subOrchestrationCreatedEventIdToParentTaskId = []; // Maps child instance id -> parent action/task id (our local task id). @@ -75,6 +76,7 @@ public WorkflowOrchestrationContext(string name, string instanceId, DateTime cur IWorkflowSerializer workflowSerializer, ILoggerFactory loggerFactory, WorkflowVersionTracker versionTracker, string? appId = null) { _workflowSerializer = workflowSerializer; + _loggerFactory = loggerFactory; _logger = loggerFactory.CreateLogger() ?? throw new ArgumentNullException(nameof(loggerFactory)); _instanceGuid = Guid.TryParse(instanceId, out var guid) ? guid : Guid.NewGuid(); @@ -339,13 +341,16 @@ public override Guid NewGuid() } /// - public override ILogger CreateReplaySafeLogger(string categoryName) => new ReplaySafeLogger(_logger, () => IsReplaying); + public override ILogger CreateReplaySafeLogger(string categoryName) => + new ReplaySafeLogger(_loggerFactory.CreateLogger(categoryName), () => IsReplaying); /// - public override ILogger CreateReplaySafeLogger(Type type) => CreateReplaySafeLogger(type.FullName ?? type.Name); + public override ILogger CreateReplaySafeLogger(Type type) => + new ReplaySafeLogger(_loggerFactory.CreateLogger(type), () => IsReplaying); /// - public override ILogger CreateReplaySafeLogger() => CreateReplaySafeLogger(typeof(T)); + public override ILogger CreateReplaySafeLogger() => + new ReplaySafeLogger(_loggerFactory.CreateLogger(), () => IsReplaying); private Task HandleHistoryMatch(string name, HistoryEvent e, int taskId) { From 7a116d1f824987156b76d43c7960f369819839de Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Tue, 24 Feb 2026 12:50:05 -0600 Subject: [PATCH 2/2] Added unit tests and flipped flag to internal to enable them Signed-off-by: Whit Waldo --- .../Worker/Internal/ReplaySafeLogger.cs | 2 +- .../WorkflowOrchestrationContextTests.cs | 129 ++++++++++++++++++ 2 files changed, 130 insertions(+), 1 deletion(-) diff --git a/src/Dapr.Workflow/Worker/Internal/ReplaySafeLogger.cs b/src/Dapr.Workflow/Worker/Internal/ReplaySafeLogger.cs index 36acfa267..457bdcc73 100644 --- a/src/Dapr.Workflow/Worker/Internal/ReplaySafeLogger.cs +++ b/src/Dapr.Workflow/Worker/Internal/ReplaySafeLogger.cs @@ -21,7 +21,7 @@ namespace Dapr.Workflow.Worker.Internal; /// internal sealed class ReplaySafeLogger(ILogger innerLogger, Func isReplaying) : ILogger { - private readonly ILogger _innerLogger = innerLogger ?? throw new ArgumentNullException(nameof(innerLogger)); + internal readonly ILogger _innerLogger = innerLogger ?? throw new ArgumentNullException(nameof(innerLogger)); private readonly Func _isReplaying = isReplaying ?? throw new ArgumentNullException(nameof(isReplaying)); public IDisposable? BeginScope(TState state) where TState : notnull => _innerLogger.BeginScope(state); diff --git a/test/Dapr.Workflow.Test/Worker/Internal/WorkflowOrchestrationContextTests.cs b/test/Dapr.Workflow.Test/Worker/Internal/WorkflowOrchestrationContextTests.cs index 1cc8620e1..3764a2440 100644 --- a/test/Dapr.Workflow.Test/Worker/Internal/WorkflowOrchestrationContextTests.cs +++ b/test/Dapr.Workflow.Test/Worker/Internal/WorkflowOrchestrationContextTests.cs @@ -16,6 +16,7 @@ using Dapr.Workflow.Serialization; using Dapr.Workflow.Versioning; using Dapr.Workflow.Worker.Internal; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using JsonException = System.Text.Json.JsonException; @@ -795,6 +796,84 @@ public void CreateReplaySafeLogger_ShouldReturnLoggerThatIsDisabledDuringReplay( Assert.False(context.IsReplaying); Assert.True(logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Information)); } + + [Fact] + public void CreateReplaySafeLogger_StringOverload_ShouldReturnReplaySafeLogger() + { + var serializer = new JsonWorkflowSerializer(new JsonSerializerOptions(JsonSerializerDefaults.Web)); + var tracker = new WorkflowVersionTracker([]); + + var context = new WorkflowOrchestrationContext( + name: "wf", + instanceId: "i", + currentUtcDateTime: new DateTime(2025, 01, 01, 0, 0, 0, DateTimeKind.Utc), + workflowSerializer: serializer, + loggerFactory: NullLoggerFactory.Instance, + versionTracker: tracker); + + const string categoryName = "test"; + var logger = context.CreateReplaySafeLogger(categoryName); + + Assert.NotNull(logger); + Assert.IsType(logger); + + // Unfortunately, this is as far as we can take this since this creates a NullLogger and it doesn't expose the + // category name + } + + [Fact] + public void CreateReplaySafeLogger_TypeOverload_ShouldReturnReplaySafeLogger() + { + var serializer = new JsonWorkflowSerializer(new JsonSerializerOptions(JsonSerializerDefaults.Web)); + var tracker = new WorkflowVersionTracker([]); + var recordingFactory = new RecordingLoggerFactory(); + + var context = new WorkflowOrchestrationContext( + name: "wf", + instanceId: "i", + currentUtcDateTime: new DateTime(2025, 01, 01, 0, 0, 0, DateTimeKind.Utc), + workflowSerializer: serializer, + loggerFactory: recordingFactory, + versionTracker: tracker); + + var logger = context.CreateReplaySafeLogger(typeof(MyExampleType)); + + Assert.NotNull(logger); + + var replaySafeLogger = Assert.IsType(logger); + + // The inner logger's category name must match the type passed to CreateReplaySafeLogger + var innerLogger = Assert.IsType(replaySafeLogger._innerLogger); + var expectedCategoryName = typeof(MyExampleType).FullName!.Replace('+', '.'); + Assert.Equal(expectedCategoryName, innerLogger.CategoryName); + } + + [Fact] + public void CreateReplaySafeLogger_GenericOverload_ShouldReturnReplaySafeLogger() + { + var serializer = new JsonWorkflowSerializer(new JsonSerializerOptions(JsonSerializerDefaults.Web)); + var tracker = new WorkflowVersionTracker([]); + var recordingFactory = new RecordingLoggerFactory(); + + var context = new WorkflowOrchestrationContext( + name: "wf", + instanceId: "i", + currentUtcDateTime: new DateTime(2025, 01, 01, 0, 0, 0, DateTimeKind.Utc), + workflowSerializer: serializer, + loggerFactory: NullLoggerFactory.Instance, + versionTracker: tracker); + + var logger = context.CreateReplaySafeLogger(); + + Assert.NotNull(logger); + + var replaySafeLogger = Assert.IsType(logger); + + // CreateLogger() returns a Logger wrapper — verify the generic type argument is correct + var innerLoggerType = replaySafeLogger._innerLogger.GetType(); + Assert.True(innerLoggerType.IsGenericType, "Inner logger should be a generic type"); + Assert.Equal(typeof(MyExampleType), innerLoggerType.GetGenericArguments()[0]); + } [Fact] public void ContinueAsNew_ShouldAddCompleteOrchestrationAction_WithCarryoverEvents_WhenPreserveUnprocessedEventsIsTrue() @@ -1156,6 +1235,56 @@ public void CreateReplaySafeLogger_TypeAndGenericOverloads_ShouldBehaveLikeCateg Assert.True(genericLogger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Information)); } + private sealed class MyExampleType + { + } + + private sealed class RecordingLoggerFactory : ILoggerFactory + { + public string? LastCategoryName { get; private set; } + public ILogger? LastCreatedLogger { get; private set; } + + public void AddProvider(ILoggerProvider provider) { } + + public void Reset() + { + LastCategoryName = null; + LastCreatedLogger = null; + } + + public ILogger CreateLogger(string categoryName) + { + LastCategoryName = categoryName; + LastCreatedLogger = new RecordingLogger(categoryName); + return LastCreatedLogger; + } + + public void Dispose() { } + } + + private sealed class RecordingLogger(string categoryName) : ILogger + { + public string CategoryName { get; } = categoryName; + + public IDisposable? BeginScope(TState state) where TState : notnull => NullScope.Instance; + public bool IsEnabled(LogLevel logLevel) => true; + + public void Log( + LogLevel logLevel, + EventId eventId, + TState state, + Exception? exception, + Func formatter) + { + } + + private sealed class NullScope : IDisposable + { + public static readonly NullScope Instance = new(); + public void Dispose() { } + } + } + private sealed class AlwaysEnabledLoggerFactory : Microsoft.Extensions.Logging.ILoggerFactory { public void AddProvider(Microsoft.Extensions.Logging.ILoggerProvider provider) { }