-
Notifications
You must be signed in to change notification settings - Fork 53
Add ReplaySafeLoggerFactory for context wrappers #670
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
+800
−6
Merged
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
1ca7145
Add ReplaySafeLoggerFactory property for context-wrapping scenarios
torosent 7f3c338
Add ReplaySafeLoggerFactory sample and tests
torosent f888b35
Address PR feedback
torosent d6c8df4
Address PR review: prevent double-wrap and add cycle detection
torosent b118da9
Rename pattern variable to avoid shadowing field
torosent e5f9190
Fix review feedback: sln cleanup, GetInput<T> signatures, error messa…
Copilot db58f18
Increase integration test timeout from 10s to 30s
torosent 110ebd4
Remove unused field and fix parameter naming in tests
torosent File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,221 @@ | ||
| // Copyright (c) Microsoft Corporation. | ||
| // Licensed under the MIT License. | ||
|
|
||
| // This sample demonstrates how to wrap TaskOrchestrationContext and delegate LoggerFactory | ||
| // to inner.ReplaySafeLoggerFactory so wrapper helpers can log without breaking replay safety. | ||
|
|
||
| using Microsoft.DurableTask; | ||
| using Microsoft.DurableTask.Client; | ||
| using Microsoft.DurableTask.Client.AzureManaged; | ||
| using Microsoft.DurableTask.Entities; | ||
| using Microsoft.DurableTask.Worker; | ||
| using Microsoft.DurableTask.Worker.AzureManaged; | ||
| using Microsoft.Extensions.Configuration; | ||
| using Microsoft.Extensions.DependencyInjection; | ||
| using Microsoft.Extensions.Hosting; | ||
| using Microsoft.Extensions.Logging; | ||
|
|
||
| namespace ReplaySafeLoggerFactorySample; | ||
|
|
||
| static class Program | ||
| { | ||
| static async Task Main(string[] args) | ||
| { | ||
| HostApplicationBuilder builder = Host.CreateApplicationBuilder(args); | ||
|
|
||
| string? schedulerConnectionString = builder.Configuration.GetValue<string>("DURABLE_TASK_SCHEDULER_CONNECTION_STRING"); | ||
| bool useScheduler = !string.IsNullOrWhiteSpace(schedulerConnectionString); | ||
|
|
||
| ConfigureDurableTask(builder, useScheduler, schedulerConnectionString); | ||
|
|
||
| IHost host = builder.Build(); | ||
| await host.StartAsync(); | ||
|
|
||
| try | ||
| { | ||
| await using DurableTaskClient client = host.Services.GetRequiredService<DurableTaskClient>(); | ||
|
|
||
| Console.WriteLine("ReplaySafeLoggerFactory Sample"); | ||
| Console.WriteLine("================================"); | ||
| Console.WriteLine(useScheduler | ||
| ? "Configured to use Durable Task Scheduler (DTS)." | ||
| : "Configured to use local gRPC. (Set DURABLE_TASK_SCHEDULER_CONNECTION_STRING to use DTS.)"); | ||
| Console.WriteLine(); | ||
|
|
||
| string instanceId = await client.ScheduleNewOrchestrationInstanceAsync( | ||
| nameof(ReplaySafeLoggingOrchestration), | ||
| input: "Seattle"); | ||
|
|
||
| Console.WriteLine($"Started orchestration instance: {instanceId}"); | ||
|
|
||
| using CancellationTokenSource timeoutCts = new(TimeSpan.FromSeconds(60)); | ||
| OrchestrationMetadata result = await client.WaitForInstanceCompletionAsync( | ||
| instanceId, | ||
| getInputsAndOutputs: true, | ||
| timeoutCts.Token); | ||
|
|
||
| if (result.RuntimeStatus != OrchestrationRuntimeStatus.Completed) | ||
| { | ||
| throw new InvalidOperationException( | ||
| $"Expected '{nameof(OrchestrationRuntimeStatus.Completed)}' but got '{result.RuntimeStatus}'."); | ||
| } | ||
|
|
||
| Console.WriteLine($"Result: {result.ReadOutputAs<string>()}"); | ||
| Console.WriteLine(); | ||
| Console.WriteLine( | ||
| "The wrapper delegates LoggerFactory to inner.ReplaySafeLoggerFactory, " + | ||
| "so wrapper-level logging stays replay-safe."); | ||
| } | ||
| finally | ||
| { | ||
| await host.StopAsync(); | ||
| } | ||
| } | ||
|
|
||
| static void ConfigureDurableTask( | ||
| HostApplicationBuilder builder, | ||
| bool useScheduler, | ||
| string? schedulerConnectionString) | ||
| { | ||
| if (useScheduler) | ||
| { | ||
| builder.Services.AddDurableTaskClient(clientBuilder => clientBuilder.UseDurableTaskScheduler(schedulerConnectionString!)); | ||
|
|
||
| builder.Services.AddDurableTaskWorker(workerBuilder => | ||
| { | ||
| workerBuilder.AddTasks(tasks => | ||
| { | ||
| tasks.AddOrchestrator<ReplaySafeLoggingOrchestration>(); | ||
| tasks.AddActivity<SayHelloActivity>(); | ||
| }); | ||
|
|
||
| workerBuilder.UseDurableTaskScheduler(schedulerConnectionString!); | ||
| }); | ||
| } | ||
| else | ||
| { | ||
| builder.Services.AddDurableTaskClient().UseGrpc(); | ||
|
|
||
| builder.Services.AddDurableTaskWorker() | ||
| .AddTasks(tasks => | ||
| { | ||
| tasks.AddOrchestrator<ReplaySafeLoggingOrchestration>(); | ||
| tasks.AddActivity<SayHelloActivity>(); | ||
| }) | ||
| .UseGrpc(); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| [DurableTask(nameof(ReplaySafeLoggingOrchestration))] | ||
| sealed class ReplaySafeLoggingOrchestration : TaskOrchestrator<string, string> | ||
| { | ||
| public override async Task<string> RunAsync(TaskOrchestrationContext context, string input) | ||
| { | ||
| LoggingTaskOrchestrationContext wrappedContext = new(context); | ||
| ILogger logger = wrappedContext.CreateLogger<ReplaySafeLoggingOrchestration>(); | ||
|
|
||
| logger.LogInformation("Wrapping orchestration context for instance {InstanceId}.", wrappedContext.InstanceId); | ||
|
|
||
| string greeting = await wrappedContext.CallActivityWithLoggingAsync<string>(nameof(SayHelloActivity), input); | ||
|
|
||
| logger.LogInformation("Returning activity result for {InstanceId}.", wrappedContext.InstanceId); | ||
| return greeting; | ||
| } | ||
| } | ||
|
|
||
| [DurableTask(nameof(SayHelloActivity))] | ||
| sealed class SayHelloActivity : TaskActivity<string, string> | ||
| { | ||
| readonly ILogger<SayHelloActivity> logger; | ||
|
|
||
| public SayHelloActivity(ILoggerFactory loggerFactory) | ||
| { | ||
| this.logger = loggerFactory.CreateLogger<SayHelloActivity>(); | ||
| } | ||
|
|
||
| public override Task<string> RunAsync(TaskActivityContext context, string input) | ||
| { | ||
| this.logger.LogInformation("Generating a greeting for {Name}.", input); | ||
| return Task.FromResult( | ||
| $"Hello, {input}! This orchestration used ReplaySafeLoggerFactory to keep wrapper logging replay-safe."); | ||
| } | ||
| } | ||
|
|
||
| sealed class LoggingTaskOrchestrationContext : TaskOrchestrationContext | ||
| { | ||
| readonly TaskOrchestrationContext innerContext; | ||
|
|
||
| public LoggingTaskOrchestrationContext(TaskOrchestrationContext innerContext) | ||
| { | ||
| this.innerContext = innerContext ?? throw new ArgumentNullException(nameof(innerContext)); | ||
| } | ||
|
|
||
| // Only abstract members need explicit forwarding here. Virtual helpers such as | ||
| // ReplaySafeLoggerFactory and the convenience overloads continue to work through these overrides. | ||
| public override TaskName Name => this.innerContext.Name; | ||
|
|
||
| public override string InstanceId => this.innerContext.InstanceId; | ||
|
|
||
| public override ParentOrchestrationInstance? Parent => this.innerContext.Parent; | ||
|
|
||
| public override DateTime CurrentUtcDateTime => this.innerContext.CurrentUtcDateTime; | ||
|
|
||
| public override bool IsReplaying => this.innerContext.IsReplaying; | ||
|
|
||
| public override string Version => this.innerContext.Version; | ||
|
|
||
| public override IReadOnlyDictionary<string, object?> Properties => this.innerContext.Properties; | ||
|
|
||
| protected override ILoggerFactory LoggerFactory => this.innerContext.ReplaySafeLoggerFactory; | ||
|
|
||
| public ILogger CreateLogger<T>() | ||
| => this.CreateReplaySafeLogger<T>(); | ||
|
|
||
| public async Task<TResult> CallActivityWithLoggingAsync<TResult>( | ||
| TaskName name, | ||
| object? input = null, | ||
| TaskOptions? options = null) | ||
| { | ||
| ILogger logger = this.CreateReplaySafeLogger<LoggingTaskOrchestrationContext>(); | ||
| logger.LogInformation("Calling activity {ActivityName} for instance {InstanceId}.", name.Name, this.InstanceId); | ||
|
|
||
| TResult result = await this.CallActivityAsync<TResult>(name, input, options); | ||
|
|
||
| logger.LogInformation("Activity {ActivityName} completed for instance {InstanceId}.", name.Name, this.InstanceId); | ||
| return result; | ||
| } | ||
|
|
||
| public override T GetInput<T>() | ||
| => this.innerContext.GetInput<T>()!; | ||
|
|
||
torosent marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| public override Task<TResult> CallActivityAsync<TResult>( | ||
| TaskName name, | ||
| object? input = null, | ||
| TaskOptions? options = null) | ||
| => this.innerContext.CallActivityAsync<TResult>(name, input, options); | ||
|
|
||
| public override Task CreateTimer(DateTime fireAt, CancellationToken cancellationToken) | ||
| => this.innerContext.CreateTimer(fireAt, cancellationToken); | ||
|
|
||
| public override Task<T> WaitForExternalEvent<T>(string eventName, CancellationToken cancellationToken = default) | ||
| => this.innerContext.WaitForExternalEvent<T>(eventName, cancellationToken); | ||
|
|
||
| public override void SendEvent(string instanceId, string eventName, object payload) | ||
| => this.innerContext.SendEvent(instanceId, eventName, payload); | ||
|
|
||
| public override void SetCustomStatus(object? customStatus) | ||
| => this.innerContext.SetCustomStatus(customStatus); | ||
|
|
||
| public override Task<TResult> CallSubOrchestratorAsync<TResult>( | ||
| TaskName orchestratorName, | ||
| object? input = null, | ||
| TaskOptions? options = null) | ||
| => this.innerContext.CallSubOrchestratorAsync<TResult>(orchestratorName, input, options); | ||
|
|
||
| public override void ContinueAsNew(object? newInput = null, bool preserveUnprocessedEvents = true) | ||
| => this.innerContext.ContinueAsNew(newInput, preserveUnprocessedEvents); | ||
|
|
||
| public override Guid NewGuid() | ||
| => this.innerContext.NewGuid(); | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,78 @@ | ||
| # Replay-Safe Logger Factory Sample | ||
|
|
||
| This sample demonstrates how to wrap `TaskOrchestrationContext` and use the new `ReplaySafeLoggerFactory` property to preserve replay-safe logging. | ||
|
|
||
| ## Overview | ||
|
|
||
| When you build helper libraries or decorators around `TaskOrchestrationContext`, C# protected access rules prevent you from delegating the protected `LoggerFactory` property from an inner context. This sample shows the recommended pattern: | ||
|
|
||
| ```csharp | ||
| protected override ILoggerFactory LoggerFactory => innerContext.ReplaySafeLoggerFactory; | ||
| ``` | ||
|
|
||
| That approach keeps wrapper-level logging replay-safe while still allowing the wrapper to add orchestration-specific helper methods. | ||
|
|
||
| ## What This Sample Does | ||
|
|
||
| 1. Defines a `LoggingTaskOrchestrationContext` wrapper around `TaskOrchestrationContext` | ||
| 2. Delegates the wrapper's `LoggerFactory` to `innerContext.ReplaySafeLoggerFactory` | ||
| 3. Adds a `CallActivityWithLoggingAsync` helper that logs before and after an activity call | ||
| 4. Runs an orchestration that uses the wrapper and completes with a simple greeting | ||
|
|
||
| ## Running the Sample | ||
|
|
||
| This sample can run against either: | ||
|
|
||
| 1. **Durable Task Scheduler (DTS)**: set the `DURABLE_TASK_SCHEDULER_CONNECTION_STRING` environment variable. | ||
| 2. **Local gRPC endpoint**: if the environment variable is not set, the sample uses the default local gRPC configuration. | ||
|
|
||
| ### DTS | ||
|
|
||
| Set `DURABLE_TASK_SCHEDULER_CONNECTION_STRING` and run the sample. | ||
|
|
||
| ```cmd | ||
| set DURABLE_TASK_SCHEDULER_CONNECTION_STRING=Endpoint=https://...;TaskHub=...;Authentication=...; | ||
| dotnet run --project samples/ReplaySafeLoggerFactorySample/ReplaySafeLoggerFactorySample.csproj | ||
| ``` | ||
|
|
||
| ```bash | ||
| export DURABLE_TASK_SCHEDULER_CONNECTION_STRING="Endpoint=https://...;TaskHub=...;Authentication=...;" | ||
| dotnet run --project samples/ReplaySafeLoggerFactorySample/ReplaySafeLoggerFactorySample.csproj | ||
| ``` | ||
|
|
||
| ## Expected Output | ||
|
|
||
| The sample: | ||
|
|
||
| 1. Starts a simple orchestration | ||
| 2. Wraps the orchestration context | ||
| 3. Calls an activity through a wrapper helper that uses replay-safe logging | ||
| 4. Prints the orchestration result | ||
|
|
||
| ## Code Structure | ||
|
|
||
| - `Program.cs`: Contains the host setup, orchestration, activity, and wrapper context | ||
|
|
||
| ## Key Code Snippet | ||
|
|
||
| ```csharp | ||
| internal sealed class LoggingTaskOrchestrationContext : TaskOrchestrationContext | ||
| { | ||
| protected override ILoggerFactory LoggerFactory => this.innerContext.ReplaySafeLoggerFactory; | ||
|
|
||
| public async Task<TResult> CallActivityWithLoggingAsync<TResult>(TaskName name, object? input = null) | ||
| { | ||
| ILogger logger = this.CreateReplaySafeLogger<LoggingTaskOrchestrationContext>(); | ||
| logger.LogInformation("Calling activity {ActivityName}.", name.Name); | ||
| TResult result = await this.CallActivityAsync<TResult>(name, input); | ||
| logger.LogInformation("Activity {ActivityName} completed.", name.Name); | ||
| return result; | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| ## Notes | ||
|
|
||
| - The key design point is that the raw `LoggerFactory` remains protected on `TaskOrchestrationContext` | ||
| - `ReplaySafeLoggerFactory` exists specifically for wrapper and delegation scenarios like this one | ||
| - The wrapper shown here forwards the core abstract members needed by the sample; real wrappers can forward additional members as needed |
28 changes: 28 additions & 0 deletions
28
samples/ReplaySafeLoggerFactorySample/ReplaySafeLoggerFactorySample.csproj
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| <Project Sdk="Microsoft.NET.Sdk"> | ||
|
|
||
| <PropertyGroup> | ||
| <OutputType>Exe</OutputType> | ||
| <TargetFrameworks>net6.0;net8.0;net10.0</TargetFrameworks> | ||
| <Nullable>enable</Nullable> | ||
| </PropertyGroup> | ||
|
|
||
| <ItemGroup> | ||
| <PackageReference Include="Microsoft.Extensions.Hosting" /> | ||
|
|
||
| <!-- Real projects would use package references --> | ||
| <!-- | ||
| <PackageReference Include="Microsoft.DurableTask.Client.Grpc" Version="1.5.0" /> | ||
| <PackageReference Include="Microsoft.DurableTask.Worker.Grpc" Version="1.5.0" /> | ||
| --> | ||
| </ItemGroup> | ||
|
|
||
| <ItemGroup> | ||
| <!-- Using p2p references so we can show latest changes in samples. --> | ||
| <ProjectReference Include="$(SrcRoot)Client/Grpc/Client.Grpc.csproj" /> | ||
| <ProjectReference Include="$(SrcRoot)Client/AzureManaged/Client.AzureManaged.csproj" /> | ||
| <ProjectReference Include="$(SrcRoot)Worker/Grpc/Worker.Grpc.csproj" /> | ||
| <ProjectReference Include="$(SrcRoot)Worker/AzureManaged/Worker.AzureManaged.csproj" /> | ||
| <ProjectReference Include="$(SrcRoot)Analyzers/Analyzers.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" /> | ||
| </ItemGroup> | ||
|
|
||
| </Project> |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.