Skip to content

Add replay-aware logger to Amazon.Lambda.DurableExecution#2371

Draft
GarrettBeatty wants to merge 1 commit into
GarrettBeatty/stack/3from
GarrettBeatty/stack/4
Draft

Add replay-aware logger to Amazon.Lambda.DurableExecution#2371
GarrettBeatty wants to merge 1 commit into
GarrettBeatty/stack/3from
GarrettBeatty/stack/4

Conversation

@GarrettBeatty
Copy link
Copy Markdown
Contributor

@GarrettBeatty GarrettBeatty commented May 14, 2026

#2216


What

Implements context.Logger, the replay-aware ILogger described in Docs/durable-execution-design.md and shipped by the Python / Java / JavaScript reference SDKs.

Public API surface introduced:

Type Purpose
IDurableContext.Logger Replay-safe ILogger (was NullLogger.Instance).
IDurableContext.ConfigureLogger(LoggerConfig) Swap the inner logger and/or disable replay-aware filtering.
LoggerConfig CustomLogger + ModeAware configuration record.

Why

Without replay-aware logging, every Console.WriteLine (or any non-suppressing logger) repeats on every replay invocation. A 30-step workflow re-invoked 30 times produces 30 copies of every log line — noisy at best, misleading at worst. The reference SDKs all solve this by reading replay state on each log call and suppressing emission while the workflow is re-deriving prior operations from checkpoint state. This PR ports that behavior to .NET on top of the per-operation replay tracker introduced in #2360.

How

ReplayAwareLogger. An ILogger decorator that consults ExecutionState.IsReplaying on every call. Short-circuits both Log<TState> and IsEnabled during replay so LoggerExtensions.LogXxx doesn't even format the message string. BeginScope always passes through so the scope stack stays balanced — suppression only applies at log emission.

Default inner logger. LambdaCoreLogger — a minimal in-package adapter that delegates to Amazon.Lambda.Core.LambdaLogger.Log, so logs flow into the standard Lambda runtime pipeline (JSON when AWS_LAMBDA_LOG_FORMAT=JSON, level-filtered by AWS_LAMBDA_LOG_LEVEL). Avoids forcing a dependency on Amazon.Lambda.Logging.AspNetCore. Users who want Serilog/Powertools/etc. swap their own logger via ConfigureLogger.

Metadata scopes. DurableFunction.WrapAsyncCore opens a BeginScope around the workflow body carrying durableExecutionArn + awsRequestId. StepOperation opens a per-step scope (operationId, operationName, attempt) around the user-func invocation only. Structured log providers tag every log line emitted by user code with that metadata automatically.

Key files:

  • LoggerConfig.cs — public configuration type
  • Internal/ReplayAwareLogger.cs — the decorator
  • Internal/LambdaCoreLogger.cs — default inner logger
  • DurableContext.cs — replaces NullLogger default; implements ConfigureLogger
  • DurableFunction.cs — execution-level scope
  • Internal/StepOperation.cs — step-level scope around user func

Testing

Unit tests (10 new in Amazon.Lambda.DurableExecution.Tests):

  • ReplayAwareLoggerTests (7) — replay suppression, execution passthrough, ModeAware=false, IsEnabled short-circuit, BeginScope passthrough, mid-workflow REPLAY → NEW transition (mirrors Python's test_logger_replay_then_new_logging).
  • DurableContextTests (3) — Logger_Default_IsReplayAwareLogger, ConfigureLogger_WithCustomLogger_ReachesUserLogger, ConfigureLogger_ModeAwareFalse_LogsDuringReplay.

Integration test (ReplayAwareLoggerTest in Amazon.Lambda.DurableExecution.IntegrationTests):

End-to-end proof on real AWS infra. Deploys a step → wait(3s) → step workflow that pairs each context.Logger.LogInformation line with a Console.WriteLine "control" line. After the durable execution completes (across two invocations driven by the wait), queries CloudWatch Logs and asserts:

  • Each replay-aware line appears exactly once across both invocations.
  • Each control line appears once per invocation that reached it (proving the function genuinely replayed).

This pins the suppression contract end-to-end against the actual durable-execution service.

Out of scope (follow-up PRs)

  • MapAsync / ParallelAsync / RunInChildContextAsync / WaitForConditionAsync
  • CallbackAsync, InvokeAsync
  • DefaultJsonCheckpointSerializer
  • Annotations source-generator integration / [DurableExecution] attribute
  • DurableTestRunner / Amazon.Lambda.DurableExecution.Testing package
  • dotnet new lambda.DurableFunction blueprint


COPY bin/publish/ ${LAMBDA_TASK_ROOT}

ENTRYPOINT ["/var/task/bootstrap"]
@GarrettBeatty GarrettBeatty force-pushed the GarrettBeatty/stack/3 branch from 7ca2099 to 5a29b3e Compare May 17, 2026 20:16
@GarrettBeatty GarrettBeatty force-pushed the GarrettBeatty/stack/4 branch from f74f35a to 0ad914a Compare May 18, 2026 01:22
Implement context.Logger, the replay-aware ILogger described in
Docs/durable-execution-design.md and shipped by the Python / Java / JS
reference SDKs. Messages emitted while the workflow is replaying prior
operations are suppressed, so a 30-step workflow re-invoked 30 times
emits each LogInformation line once instead of 30 times.

Public API:
- IDurableContext.Logger — was NullLogger.Instance, now a replay-safe
  ILogger backed by Amazon.Lambda.Core.LambdaLogger so logs flow into
  the standard runtime pipeline (JSON when AWS_LAMBDA_LOG_FORMAT=JSON,
  level-filtered by AWS_LAMBDA_LOG_LEVEL).
- IDurableContext.ConfigureLogger(LoggerConfig) — swap the inner
  ILogger (Serilog, Powertools, etc.) and/or disable replay-aware
  filtering (ModeAware = false) for debugging. Matches the API shape
  documented in the design doc.

Internals:
- ReplayAwareLogger — ILogger decorator that consults
  ExecutionState.IsReplaying on every Log call. Short-circuits both
  Log<TState> and IsEnabled during replay so LoggerExtensions.LogXxx
  doesn't even format the string. BeginScope always passes through so
  the scope stack stays balanced.
- LambdaCoreLogger — minimal in-package adapter that delegates to
  Amazon.Lambda.Core.LambdaLogger.Log. Avoids forcing a dependency on
  Amazon.Lambda.Logging.AspNetCore.
- DurableFunction.WrapAsyncCore opens a BeginScope around the workflow
  body carrying durableExecutionArn + awsRequestId. StepOperation
  opens a per-step scope (operationId, operationName, attempt) around
  the user-func invocation only. Structured log providers (the
  runtime's JSON formatter, Serilog, etc.) tag every log line emitted
  by user code with that metadata automatically.

Tests:
- ReplayAwareLoggerTests — 7 unit tests: replay suppression, execution
  passthrough, ModeAware=false, IsEnabled short-circuit, scope
  passthrough, mid-workflow REPLAY→NEW transition (mirrors Python's
  test_logger_replay_then_new_logging).
- DurableContextTests — coverage for the default logger, ConfigureLogger
  with a custom logger, and ConfigureLogger { ModeAware = false }
  enabling logs during replay.
- ReplayAwareLoggerTest (integration) — deploys a Step → Wait → Step
  workflow that pairs each context.Logger.LogInformation line with a
  Console.WriteLine "control" line. After the durable execution
  completes, queries CloudWatch Logs and asserts each replay-aware
  line appears exactly once across both invocations while each control
  line appears once per invocation, proving the suppression works
  end-to-end.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a replay-aware ILogger implementation to Amazon.Lambda.DurableExecution so workflow logs don’t duplicate during replay, and exposes a small configuration surface for swapping the underlying logger and toggling replay filtering.

Changes:

  • Introduces ReplayAwareLogger + LambdaCoreLogger, wires IDurableContext.Logger to be replay-aware by default, and adds IDurableContext.ConfigureLogger(LoggerConfig).
  • Adds execution- and step-level BeginScope metadata for structured loggers.
  • Adds unit tests and a CloudWatch-based integration test to validate replay suppression end-to-end.

Reviewed changes

Copilot reviewed 14 out of 14 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
Libraries/src/Amazon.Lambda.DurableExecution/IDurableContext.cs Updates public context API with replay-safe Logger docs and new ConfigureLogger(LoggerConfig) method.
Libraries/src/Amazon.Lambda.DurableExecution/LoggerConfig.cs Adds public configuration type for swapping inner logger and toggling replay-aware suppression.
Libraries/src/Amazon.Lambda.DurableExecution/Internal/ReplayAwareLogger.cs Adds replay-suppressing ILogger decorator driven by ExecutionState.IsReplaying.
Libraries/src/Amazon.Lambda.DurableExecution/Internal/LambdaCoreLogger.cs Adds default in-package logger adapter that routes to Amazon.Lambda.Core.LambdaLogger.
Libraries/src/Amazon.Lambda.DurableExecution/DurableContext.cs Defaults Logger to replay-aware logger and implements ConfigureLogger.
Libraries/src/Amazon.Lambda.DurableExecution/DurableFunction.cs Adds execution-level logging scope for structured metadata.
Libraries/src/Amazon.Lambda.DurableExecution/Internal/StepOperation.cs Adds step-level logging scope for operation metadata around user step invocation.
Libraries/test/Amazon.Lambda.DurableExecution.Tests/Internal/ReplayAwareLoggerTests.cs Adds unit tests for replay suppression, scope passthrough, and mode transitions.
Libraries/test/Amazon.Lambda.DurableExecution.Tests/DurableContextTests.cs Adds unit tests for default logger type and ConfigureLogger behavior.
Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/Amazon.Lambda.DurableExecution.IntegrationTests.csproj Adds CloudWatch Logs SDK dependency for log verification.
Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/ReplayAwareLoggerTest.cs Adds CloudWatch-based integration test validating replay suppression vs Console control lines.
Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/ReplayAwareLoggerFunction/ReplayAwareLoggerFunction.csproj Adds new integration-test Lambda function project.
Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/ReplayAwareLoggerFunction/Function.cs Implements Step→Wait→Step workflow emitting replay-aware and control log markers.
Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests/TestFunctions/ReplayAwareLoggerFunction/Dockerfile Adds container packaging for the new integration-test function.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

// expected count of every marker so the test never short-circuits with
// a still-arriving "after_step1" record (which is emitted at a
// different timestamp than workflow_start and indexes independently).
using var logs = new AmazonCloudWatchLogsClient(RegionEndpoint.USEast1);
Comment on lines +32 to +33
// Level filtering is performed by the runtime layer (AWS_LAMBDA_LOG_LEVEL).
public bool IsEnabled(LogLevel logLevel) => logLevel != LogLevel.None;
Comment on lines +44 to +54
var message = formatter(state, exception);
var levelName = logLevel.ToString();

if (exception != null)
{
CoreLambdaLogger.Log(levelName, exception, message);
}
else
{
CoreLambdaLogger.Log(levelName, message);
}
Comment on lines +117 to +121
using (context.Logger.BeginScope(new Dictionary<string, object>
{
["durableExecutionArn"] = invocationInput.DurableExecutionArn,
["awsRequestId"] = lambdaContext.AwsRequestId ?? string.Empty,
}))
Comment on lines +203 to +207
using (_logger.BeginScope(new Dictionary<string, object>
{
["operationId"] = OperationId,
["operationName"] = Name ?? string.Empty,
["attempt"] = attemptNumber,
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants