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;
}
}
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.
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:
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:
executionArn(TypeScript, Python) /durableExecutionArn(Java MDC) the 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).
contextIdthe operation ID of the child context operationcontextNamethe 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";
});
}
}
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.
Custom logger¶
You can replace the default logger with any logger that implements the SDK's logger interface.
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.
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) {
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.
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.
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";
}
}