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

Stacked PRs:


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

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 bin/publish/ ${LAMBDA_TASK_ROOT}

ENTRYPOINT ["/var/task/bootstrap"]
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.

2 participants