Logging¶
Replay-aware logger¶
Use the Durable Execution SDK logger to add structured log entries to your function. The SDK automatically tags every entry with execution metadata such as the ARN, operation name, and retry attempt. The logger will not emit duplicate log entries on replay. The SDK provides a default logger, or you can provide a custom logger.
The Powertools for AWS Lambda logger works as a replacement for the SDK's default logger. It adds structured JSON output, correlation IDs, log sampling, and X-Ray tracing integration.
import { DurableContext, withDurableExecution } from "@aws/durable-execution-sdk-js";
export const handler = withDurableExecution(async (event, context: DurableContext) => {
context.logger.info("Starting workflow");
const result = await context.step("process", async () => {
return "done";
});
context.logger.info("Workflow complete", { result });
return result;
});
from aws_durable_execution_sdk_python import DurableContext, durable_execution
@durable_execution
def handler(event: dict, context: DurableContext):
context.logger.info("Starting workflow")
result = context.step(lambda ctx: "done")
context.logger.info("Workflow complete", extra={"result": result})
return result
import software.amazon.lambda.durable.DurableContext;
import software.amazon.lambda.durable.DurableHandler;
public class BasicUsage extends DurableHandler<Object, String> {
@Override
public String handleRequest(Object event, DurableContext context) {
context.getLogger().info("Starting workflow");
String result = context.step("process", String.class, stepCtx -> "done");
context.getLogger().info("Workflow complete: {}", result);
return result;
}
}
ctx.Logger() returns a *slog.Logger. Its records carry requestId,
executionArn, and tenantId when the invocation has one. The default
handler writes one JSON object per record to stderr.
package main
import "github.com/aws/aws-durable-execution-sdk-go/durable"
func handler(ctx durable.Context, event map[string]any) (string, error) {
ctx.Logger().Info("Starting workflow")
result, err := durable.Step(ctx, "process", func(sc durable.StepContext) (string, error) {
return "done", nil
})
if err != nil {
return "", err
}
ctx.Logger().Info("Workflow complete", "result", result)
return result, nil
}
func main() {
durable.Start(handler)
}
using Amazon.Lambda.Core;
using Amazon.Lambda.DurableExecution;
using Microsoft.Extensions.Logging;
public class BasicUsageExample
{
public Task<DurableExecutionInvocationOutput> Handler(
DurableExecutionInvocationInput input, ILambdaContext context)
=> DurableFunction.WrapAsync<object, string>(Workflow, input, context);
private async Task<string> Workflow(object input, IDurableContext ctx)
{
ctx.Logger.LogInformation("Starting workflow");
string result = await ctx.StepAsync(
async (_, _) => "done",
name: "process");
ctx.Logger.LogInformation("Workflow complete: {Result}", result);
return result;
}
}
Log methods¶
// On DurableContext and StepContext:
context.logger.info(...params: unknown[]): void
context.logger.warn(...params: unknown[]): void
context.logger.error(...params: unknown[]): void
context.logger.debug(...params: unknown[]): void
context.logger.log(level: "INFO" | "WARN" | "ERROR" | "DEBUG", ...params: unknown[]): void
# On DurableContext and StepContext:
context.logger.debug(msg, *args, extra=None)
context.logger.info(msg, *args, extra=None)
context.logger.warning(msg, *args, extra=None)
context.logger.error(msg, *args, extra=None)
context.logger.exception(msg, *args, extra=None)
Parameters (all log methods):
msg(object) The log message.*args(object) Arguments for message formatting, passed to the underlying logger.extra(Mapping[str, object] | None) Additional fields to include in the log entry. These merge with the automatic context fields.
// Obtain from any context:
DurableLogger logger = context.getLogger();
DurableLogger logger = stepContext.getLogger();
// Available methods:
logger.trace(String format, Object... args)
logger.debug(String format, Object... args)
logger.info(String format, Object... args)
logger.warn(String format, Object... args)
logger.error(String format, Object... args)
logger.error(String message, Throwable t)
The Java logger uses SLF4J format strings. Pass {} placeholders and positional
arguments.
ctx.Logger() and stepCtx.Logger() return *slog.Logger, so the log
methods are the standard library's. Pass structured fields as alternating
key and value arguments, or as slog.Attr values. With the SDK's default
handler, an attribute whose value is an error expands into errorType,
errorMessage, and, when the error carries recorded frames, stackTrace.
logger := ctx.Logger()
logger.Debug("message", "key", value)
logger.Info("message", "key", value)
logger.Warn("message", "key", value)
logger.Error("message", "err", err)
logger.Log(ctx, slog.LevelInfo, "message", "key", value)
The four named slog levels are LevelDebug, LevelInfo, LevelWarn, and
LevelError.
// context.Logger and stepContext.Logger are Microsoft.Extensions.Logging.ILogger.
// Use the standard ILogger extension methods:
context.Logger.LogTrace(string message, params object?[] args);
context.Logger.LogDebug(string message, params object?[] args);
context.Logger.LogInformation(string message, params object?[] args);
context.Logger.LogWarning(string message, params object?[] args);
context.Logger.LogError(string message, params object?[] args);
context.Logger.LogError(Exception exception, string message, params object?[] args);
context.Logger.LogCritical(string message, params object?[] args);
The ILogger methods use message templates. Pass {Name} placeholders and
positional arguments.
Default log format¶
The default logger always emits structured JSON. A log entry from a step looks like:
When your Lambda function's log format is set to JSON, the log output includes the extra metadata as top-level keys in the JSON output. See Using Lambda advanced logging controls with Python.
Calling getLogger() populates SLF4J MDC with execution context fields. When your
Lambda function's log format is set to JSON and your logging framework includes MDC
fields in its output, those fields appear as top-level keys in the JSON log record. See
Using Lambda advanced logging controls with Java.
Field names and structure depend on your logging framework configuration. A Log4j2 JSON output might look like:
The SDK's default handler always writes one JSON object per record to stderr. The field names are the SDK's own, and they do not depend on Lambda advanced logging controls. A record from a step looks like:
{
"timestamp": "2025-11-21T18:39:24.743Z",
"level": "INFO",
"message": "Running step",
"requestId": "72171fff-...",
"executionArn": "arn:aws:lambda:...",
"operationId": "abc123",
"operationName": "process",
"attempt": 1
}
timestamp is ISO 8601 UTC with millisecond precision and a Z suffix.
level is DEBUG, INFO, WARN, or ERROR. An attribute whose value
is an error expands into errorType, errorMessage, and stackTrace
when the error carries recorded frames. The default handler reads its
minimum level from AWS_LAMBDA_LOG_LEVEL. An unset or unrecognized value
selects INFO. A handler you supply applies its own level.
The SDK also writes records of its own through the installed handler.
When a checkpoint response without a token suspends the invocation, the
SDK writes one WARN record. A handler enabled at DEBUG also receives
the SDK's trace of its own work, with the messages operation claimed,
replay complete; executing live, checkpoint enqueued,
checkpoint flushed, invocation suspending, and operation completed.
The handler's level is the only switch for these DEBUG records.
The SDK writes through Microsoft.Extensions.Logging.ILogger and attaches
execution context fields via ILogger.BeginScope. When your Lambda function's
log format is set to JSON and your logger provider includes scope fields in its
output, those fields appear as top-level keys in the JSON log record. See
Using Lambda advanced logging controls with C#.
Field names and structure depend on your logger provider configuration. A JSON console output might look like:
Execution metadata¶
The SDK automatically enriches log entries with execution metadata. The metadata varies depending on the logging context.
DurableContext at handler level¶
This is the DurableContext passed to the durable handler. It enriches the output with
the following fields:
executionArnthe ARN of the current durable executionrequestIdthe Lambda request ID
DurableContext (child)¶
All DurableContext fields, plus:
operationIdhashed ID of the child context operation
parentIdthe operation ID of the current child context. Operations inside this child context log bothparentId(the containing child context) andoperationId(the operation itself).
operationIdthe operation ID of the child context operationoperationNamethe name given to the child context, when you provide one
operationIdthe ID of the child context operationoperationNamethe name given to the child context, when you provide one
The SDK has one Context type. A child context comes from
RunInChildContext, Go, Map, Parallel, or WaitForCallback.
operationIdthe deterministic operation ID of the child context operationoperationNamethe name given to the child context, when you provide one
Operation context¶
The following operation contexts support the logger:
All DurableContext fields, plus:
operationIdthe unique ID of the operationoperationNamethe operation name, when you provide oneattemptthe current retry attempt number (1-indexed, steps only)
The following examples show logging from inside a step. Using the StepContext logger
instead of DurableContext's logger adds step-specific fields (operationId,
operationName, attempt) to every log entry from that step.
import { DurableContext, withDurableExecution } from "@aws/durable-execution-sdk-js";
export const handler = withDurableExecution(async (event, context: DurableContext) => {
await context.step("process", async (stepCtx) => {
// stepCtx.logger includes operationId and attempt in every log entry.
stepCtx.logger.info("Running step");
return "done";
});
});
from aws_durable_execution_sdk_python import DurableContext, StepContext, durable_execution
@durable_execution
def handler(event: dict, context: DurableContext):
def process(step_ctx: StepContext) -> str:
# step_ctx.logger includes executionArn, operationId, and attempt.
step_ctx.logger.info("Running step")
return "done"
return context.step(process, name="process")
The Java SDK uses SLF4J MDC to attach fields. Configure your logging framework (e.g. Logback, Log4j2) to include MDC fields in your log pattern.
import software.amazon.lambda.durable.DurableContext;
import software.amazon.lambda.durable.DurableHandler;
import software.amazon.lambda.durable.StepContext;
public class StepContextLogger extends DurableHandler<Object, String> {
@Override
public String handleRequest(Object event, DurableContext context) {
return context.step("process", String.class, (StepContext stepCtx) -> {
// stepCtx.getLogger() includes operationId, operationName, and attempt in MDC.
stepCtx.getLogger().info("Running step");
return "done";
});
}
}
stepCtx.Logger() returns a *slog.Logger whose records add this
operation's operationId, its operationName when it has one, and the
attempt number. The step body, the condition check, and the callback
submitter all receive the same StepContext type, and attempt is set
for all three. Inside a child context, a step's records report the step's
operationId and operationName in place of the child's.
package main
import "github.com/aws/aws-durable-execution-sdk-go/durable"
func handler(ctx durable.Context, event map[string]any) (string, error) {
return durable.Step(ctx, "process", func(sc durable.StepContext) (string, error) {
sc.Logger().Info("Running step")
return "done", nil
})
}
func main() {
durable.Start(handler)
}
The C# SDK attaches fields via ILogger.BeginScope. Configure your logger
provider to include scope fields in its output.
using Amazon.Lambda.Core;
using Amazon.Lambda.DurableExecution;
using Microsoft.Extensions.Logging;
public class StepContextLoggerExample
{
public Task<DurableExecutionInvocationOutput> Handler(
DurableExecutionInvocationInput input, ILambdaContext context)
=> DurableFunction.WrapAsync<object, string>(Workflow, input, context);
private async Task<string> Workflow(object input, IDurableContext ctx)
{
return await ctx.StepAsync(
async (stepCtx, _) =>
{
// stepCtx.Logger includes operationId, operationName, and attempt
// in every log entry from this step.
stepCtx.Logger.LogInformation("Running step");
return "done";
},
name: "process");
}
}
Replay log suppression¶
When the SDK replays, it runs your handler from the start until it reaches the next incomplete operation. It does not re-emit log entries encountered before that point, since these already emitted on the first invocation that traversed them.
Logs inside a retrying step body always emit, because the step has not completed yet.
import { DurableContext, withDurableExecution } from "@aws/durable-execution-sdk-js";
export const handler = withDurableExecution(async (event, context: DurableContext) => {
// context.logger suppresses duplicate logs during replay by default.
// Logs from completed operations do not repeat when the SDK replays.
context.logger.info("Step 1 starting");
await context.step("step-1", async () => {
return "result";
});
context.logger.info("Step 1 complete");
});
Pass modeAware: false to configureLogger to emit logs on every replay.
import { DurableContext, withDurableExecution } from "@aws/durable-execution-sdk-js";
export const handler = withDurableExecution(async (event, context: DurableContext) => {
// Pass modeAware: false to emit logs on every replay.
context.configureLogger({ modeAware: false });
context.logger.info("This logs on every replay");
await context.step("step-1", async () => {
return "result";
});
});
from aws_durable_execution_sdk_python import DurableContext, durable_execution
@durable_execution
def handler(event: dict, context: DurableContext):
# context.logger suppresses duplicate logs during replay by default.
# Logs from completed operations do not repeat when the SDK replays.
context.logger.info("Step 1 starting")
result = context.step(lambda ctx: "result")
context.logger.info("Step 1 complete", extra={"result": result})
return result
The Python SDK checks execution_state.is_replaying() before each log call. You cannot
disable this per-context, but you can log directly to the underlying logger to bypass
it.
import software.amazon.lambda.durable.DurableContext;
import software.amazon.lambda.durable.DurableHandler;
public class ReplaySuppression extends DurableHandler<Object, String> {
@Override
public String handleRequest(Object event, DurableContext context) {
// context.getLogger() suppresses duplicate logs during replay by default.
// Logs from completed operations do not repeat when the SDK replays.
context.getLogger().info("Step 1 starting");
String result = context.step("step-1", String.class, stepCtx -> "result");
context.getLogger().info("Step 1 complete: {}", result);
return result;
}
}
Pass LoggerConfig.withReplayLogging() to DurableConfig to emit logs on every replay.
See Configure logger.
package main
import "github.com/aws/aws-durable-execution-sdk-go/durable"
func handler(ctx durable.Context, event map[string]any) (string, error) {
ctx.Logger().Info("Step 1 starting")
result, err := durable.Step(ctx, "step-1", func(sc durable.StepContext) (string, error) {
return "result", nil
})
if err != nil {
return "", err
}
ctx.Logger().Info("Step 1 complete", "result", result)
return result, nil
}
func main() {
durable.Start(handler)
}
Suppression is a ReplayLogMode. Its three values are ReplayLogModeSuppress (the
default), ReplayLogModeEmit, and ReplayLogModeUnchanged.
ReplayLogModeUnchanged is the zero value. WithReplayLogMode treats it as
ReplayLogModeSuppress. To emit replayed records, each tagged replay=true, set
the mode at durable.Start:
To change the mode from inside the handler, call ConfigureLogging. See
Configure logger.
using Amazon.Lambda.Core;
using Amazon.Lambda.DurableExecution;
using Microsoft.Extensions.Logging;
public class ReplaySuppressionExample
{
public Task<DurableExecutionInvocationOutput> Handler(
DurableExecutionInvocationInput input, ILambdaContext context)
=> DurableFunction.WrapAsync<object, string>(Workflow, input, context);
private async Task<string> Workflow(object input, IDurableContext ctx)
{
// ctx.Logger suppresses duplicate logs during replay by default.
// Logs from completed operations do not repeat when the SDK replays.
ctx.Logger.LogInformation("Step 1 starting");
string result = await ctx.StepAsync(
async (_, _) => "result",
name: "step-1");
ctx.Logger.LogInformation("Step 1 complete: {Result}", result);
return result;
}
}
Call context.ConfigureLogger(new LoggerConfig { ModeAware = false }) to emit logs
on every replay. See Configure logger.
Custom logger¶
You can change where the SDK's logger sends its entries.
Any object that implements DurableLogger works. Pass it to configureLogger.
import { DurableContext, withDurableExecution } from "@aws/durable-execution-sdk-js";
import { Logger } from "@aws-lambda-powertools/logger";
const powertoolsLogger = new Logger({ serviceName: "my-service" });
export const handler = withDurableExecution(async (event, context: DurableContext) => {
context.configureLogger({ customLogger: powertoolsLogger });
context.logger.info("Using Powertools logger");
});
Any object that satisfies the LoggerInterface protocol works. Pass it to
context.set_logger().
from aws_lambda_powertools import Logger
from aws_durable_execution_sdk_python import DurableContext, durable_execution
powertools_logger = Logger(service="my-service")
@durable_execution
def handler(event: dict, context: DurableContext):
context.set_logger(powertools_logger)
context.logger.info("Using Powertools logger")
The Java SDK does not support swapping the underlying logger. getLogger() always wraps
an SLF4J logger obtained from LoggerFactory. To change logging behavior, configure
your SLF4J implementation (Logback, Log4j2) or adjust LoggerConfig via
DurableConfig. See Configure logger.
Install any slog.Handler with durable.WithLogHandler at durable.Start, or call
ConfigureLogging from inside the handler. The SDK attaches its structured
attributes to the handler you supply and keeps its replay suppression.
package main
import (
"log/slog"
"os"
"github.com/aws/aws-durable-execution-sdk-go/durable"
)
func handler(ctx durable.Context, event map[string]any) (string, error) {
ctx.Logger().Info("Using a custom slog handler")
return "done", nil
}
func main() {
logHandler := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelInfo})
durable.Start(handler, durable.WithLogHandler(logHandler))
}
Configure logger¶
Configure the logger on the handler's DurableContext.
LoggerConfig
interface LoggerConfig<Logger extends DurableLogger> {
customLogger?: Logger;
modeAware?: boolean;
}
LoggerConfig parameters:
customLogger(optional) ADurableLoggerimplementation to use instead of the default console logger.modeAware(optional) Whentrue(default), the SDK suppresses logs during replay. Set tofalseto emit logs on every replay.
Set the logger on the handler's DurableContext.
Pass any object that satisfies the LoggerInterface protocol.
Configure replay suppression via LoggerConfig on DurableConfig.
LoggerConfig
public record LoggerConfig(boolean suppressReplayLogs, boolean oldKeyNames) {
public static LoggerConfig defaults() // suppress replay logs (default)
public static LoggerConfig withReplayLogging() // allow logs during replay
}
LoggerConfig parameters:
suppressReplayLogs(boolean) Whentrue(default), the SDK suppresses logs during replay. UseLoggerConfig.withReplayLogging()to set this tofalse.oldKeyNames(boolean) Whenfalse(default), the SDK uses the Java 2.x MDC keysexecutionArn,operationId, andoperationName. Set it totruetemporarily to emit the 1.x keysdurableExecutionArn,contextId, andcontextNamewhile migrating log queries and dashboards.
Set the handler and the replay mode at construction with WithLogHandler
and WithReplayLogMode. To choose them inside the handler, for example
from the event payload, call ConfigureLogging. At durable.Start, a
nil handler selects the default handler, and ReplayLogModeUnchanged
selects ReplayLogModeSuppress.
func WithLogHandler(h slog.Handler) HandlerOption
func WithReplayLogMode(mode ReplayLogMode) HandlerOption
type LogConfig struct {
Handler slog.Handler
ReplayLogMode ReplayLogMode
// Has unexported fields.
}
func ConfigureLogging(ctx Context, cfg LogConfig) error
LogConfig fields:
Handler(slog.Handler) The handler behindContext.LoggerandStepContext.Logger. The SDK attaches its execution and operation attributes to it and wraps it with replay suppression.nilkeeps the current handler.ReplayLogMode(ReplayLogMode) The treatment of records emitted during replay.ReplayLogModeUnchanged, the zero value, keeps the current mode.
ConfigureLogging applies to ctx and to every child context and branch
derived after the call, for the current invocation only. A logger you
obtained before the call keeps the previous handler. A new replay mode
reaches every logger of ctx, including loggers obtained before the call.
ConfigureLogging claims no operation ID and writes no checkpoint, so a
conditional call does not make replay non-deterministic. Called off the goroutine that owns ctx, it
returns an error that wraps ErrWrongGoroutine. It also returns an error
for a Context the SDK did not create. It returns no other error.
Configure the logger on the handler's IDurableContext.
LoggerConfig
public sealed class LoggerConfig
{
public ILogger? CustomLogger { get; init; } // null = keep current logger
public bool ModeAware { get; init; } = true;
}
LoggerConfig parameters:
CustomLogger(optional) AnILoggerto use instead of the SDK default. When null, the durable context keeps its existing inner logger.ModeAware(optional) Whentrue(default), the SDK suppresses logs during replay. Set tofalseto emit logs on every replay.
Logger interface¶
DurableLogger is the interface a custom logger must implement.
import { DurableLogger, DurableLoggingContext } from "@aws/durable-execution-sdk-js";
// DurableLogger interface — implement this to use a custom logger.
interface DurableLogger {
log?(level: "INFO" | "WARN" | "ERROR" | "DEBUG", ...params: unknown[]): void;
error(...params: unknown[]): void;
warn(...params: unknown[]): void;
info(...params: unknown[]): void;
debug(...params: unknown[]): void;
configureDurableLoggingContext?(context: DurableLoggingContext): void;
}
LoggerInterface is the protocol a custom logger must satisfy.
from collections.abc import Mapping
from typing import Protocol
class LoggerInterface(Protocol):
def debug(
self, msg: object, *args: object, extra: Mapping[str, object] | None = None
) -> None: ...
def info(
self, msg: object, *args: object, extra: Mapping[str, object] | None = None
) -> None: ...
def warning(
self, msg: object, *args: object, extra: Mapping[str, object] | None = None
) -> None: ...
def error(
self, msg: object, *args: object, extra: Mapping[str, object] | None = None
) -> None: ...
def exception(
self, msg: object, *args: object, extra: Mapping[str, object] | None = None
) -> None: ...
The Java SDK wraps any SLF4J Logger in DurableLogger. There is no interface to
implement.
The logger interface is the standard library slog.Handler. Implement it, or wrap
an existing handler, and install it with durable.WithLogHandler. The SDK attaches
its structured attributes through the handler's WithAttrs method.
The C# SDK uses Microsoft.Extensions.Logging.ILogger directly. There is no
SDK-specific interface to implement; pass any ILogger as the CustomLogger on
LoggerConfig.
Powertools for AWS Lambda¶
Powertools for AWS Lambda provides a structured logger that works as a drop-in replacement for the SDK's default logger.
The
Powertools for AWS Lambda (TypeScript)
Logger satisfies the DurableLogger interface. Pass it via configureLogger.
import { Logger } from '@aws-lambda-powertools/logger';
import { DurableContext, withDurableExecution } from "@aws/durable-execution-sdk-js";
const powertoolsLogger = new Logger({ serviceName: 'my-service' });
export const handler = withDurableExecution(async (event, context: DurableContext) => {
context.configureLogger({ customLogger: powertoolsLogger });
context.logger.info('Running handler');
});
The
Powertools for AWS Lambda (Python)
Logger satisfies the LoggerInterface protocol. Pass it via context.set_logger().
from aws_lambda_powertools import Logger
from aws_durable_execution_sdk_python import DurableContext, durable_execution
powertools_logger = Logger(service="my-service")
@durable_execution
def handler(event: dict, context: DurableContext):
context.set_logger(powertools_logger)
context.logger.info("Running handler")
The
Powertools for AWS Lambda (Java) logger
uses SLF4J, which is the same logging facade the SDK wraps. Add Powertools as your SLF4J
implementation and the SDK's getLogger() will automatically use it. No additional
wiring is required.
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import software.amazon.lambda.powertools.logging.Logging;
import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.RequestHandler;
public class MyHandler implements RequestHandler<Object, String> {
private static final Logger logger = LoggerFactory.getLogger(MyHandler.class);
@Logging
public String handleRequest(Object event, Context context) {
logger.info("Running handler");
return "ok";
}
}
Powertools for AWS Lambda has no Go version. For structured JSON output, install any
slog.Handler, such as slog.NewJSONHandler or a third-party handler, with
durable.WithLogHandler. See Custom logger.
The
Powertools for AWS Lambda (.NET) logger
is a Microsoft.Extensions.Logging.ILogger. Pass it as the CustomLogger via
context.ConfigureLogger.
using Amazon.Lambda.Core;
using Amazon.Lambda.DurableExecution;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
public class PowertoolsLoggerExample
{
// The Powertools for AWS Lambda (.NET) logger is an ILogger, so it plugs in
// as the CustomLogger. Replace this placeholder with the Powertools logger,
// for example the ILogger produced by services.AddPowertoolsLogging(...).
private static readonly ILogger PowertoolsLogger = NullLogger.Instance;
public Task<DurableExecutionInvocationOutput> Handler(
DurableExecutionInvocationInput input, ILambdaContext context)
=> DurableFunction.WrapAsync<object, string>(Workflow, input, context);
private async Task<string> Workflow(object input, IDurableContext ctx)
{
ctx.ConfigureLogger(new LoggerConfig { CustomLogger = PowertoolsLogger });
ctx.Logger.LogInformation("Running handler");
return await Task.FromResult("ok");
}
}