Skip to content

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)
}

Log with the macros of tracing, the instrumentation crate the SDK uses, such as tracing::info!. A record written during an invocation carries executionArn, requestId, and tenantId when the invocation has one. A subscriber is the tracing component that formats and writes records. Call durable::init_default_subscriber() once in main to install the SDK's default subscriber, which writes each record between operations once per execution. The SDK installs no subscriber on its own.

use aws_durable_execution_sdk as durable;

async fn handler(
    _event: serde_json::Value,
    ctx: durable::DurableContext,
) -> Result<String, durable::BoxError> {
    tracing::info!("Starting workflow");

    let result = ctx
        .step(|_| async { Ok("done".to_owned()) })
        .name("process")
        .await?;

    tracing::info!(result = %result, "Workflow complete");
    Ok(result)
}

#[tokio::main]
async fn main() -> Result<(), lambda_runtime::Error> {
    durable::init_default_subscriber()?;
    durable::run(handler).await
}
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.

Log with the tracing macros. Pass structured fields as key = value pairs before the message. %value records a field with its Display form, and ?value with its Debug form.

tracing::trace!(key = value, "message");
tracing::debug!(key = value, "message");
tracing::info!(key = value, "message");
tracing::warn!(key = value, "message");
tracing::error!(key = value, "message");
tracing::event!(tracing::Level::INFO, key = value, "message");

The five levels are TRACE, DEBUG, INFO, WARN, and ERROR. The same macros work in the handler, in a step body, in a condition check, and in a child context body. The SDK adds the fields of the place where the macro runs.

// 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:

{
  "timestamp": "2025-11-21T18:39:24.743Z",
  "level": "INFO",
  "requestId": "72171fff-...",
  "executionArn": "arn:aws:lambda:...",
  "operationId": "abc123",
  "operationName": "process",
  "attempt": 1,
  "message": "Running step"
}

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.

{
  "timestamp": "2025-11-21T18:39:24Z",
  "level": "INFO",
  "message": "Running step",
  "requestId": "72171fff-...",
  "executionArn": "arn:aws:lambda:...",
  "operationId": "abc123",
  "operationName": "process",
  "attempt": 1
}

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:

{
  "timestamp": "2025-11-21T18:39:24.743Z",
  "level": "INFO",
  "message": "Running step",
  "AWSRequestId": "72171fff-...",
  "executionArn": "arn:aws:lambda:...",
  "operationId": "abc123",
  "operationName": "process",
  "attempt": "1"
}

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 default subscriber writes one JSON object per record when AWS_LAMBDA_LOG_FORMAT is JSON in any case, and text otherwise. Lambda gives a durable function the JSON log format by default, so the default subscriber writes JSON there. Each record is one line with every field at the top level. The keys come in the order requestId, timestamp, level, executionArn, tenantId, operationId, operationName, attempt, then the other fields of the record and of its spans, and message last. A key without a value is left out, except timestamp, level, and message. On Lambda the other fields include the runtime's xrayTraceId. A record from a step looks like:

{
  "requestId": "cf9500de-...",
  "timestamp": "2026-11-08T08:40:32.208Z",
  "level": "INFO",
  "executionArn": "arn:aws:lambda:...",
  "operationId": "6b86b273...",
  "operationName": "process",
  "attempt": 1,
  "xrayTraceId": "Root=1-...",
  "message": "Running step"
}

timestamp is ISO 8601 UTC with millisecond precision and a Z suffix. level is TRACE, DEBUG, INFO, WARN, or ERROR. message is "" for a record without one.

RUST_LOG gives the directives of an EnvFilter, such as info,my_crate=debug. When RUST_LOG is unset or holds only invalid directives, the level is AWS_LAMBDA_LOG_LEVEL, else INFO. A level that does not parse selects INFO. The subscriber writes to stdout.

The SDK also writes records of its own. At DEBUG it writes lifecycle records on the target aws_durable_execution_sdk::lifecycle, with the messages operation_started, operation_succeeded, operation_failed, operation_retry_scheduled, operation_replayed, execution_started, execution_resumed, and execution_suspended. A plugin hook that fails writes one WARN record on the target aws_durable_execution_sdk::plugin.

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:

{
  "Timestamp": "2025-11-21T18:39:24.743Z",
  "LogLevel": "Information",
  "Message": "Running step",
  "requestId": "72171fff-...",
  "executionArn": "arn:aws:lambda:...",
  "operationId": "abc123",
  "operationName": "process",
  "attempt": 1
}

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 the ARN of the current durable execution
  • requestId the Lambda request ID

DurableContext (child)

All DurableContext fields, plus:

  • operationId hashed ID of the child context operation
  • parentId the operation ID of the current child context. Operations inside this child context log both parentId (the containing child context) and operationId (the operation itself).
  • operationId the operation ID of the child context operation
  • operationName the name given to the child context, when you provide one
  • operationId the ID of the child context operation
  • operationName the 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.

  • operationId the ID of the child context operation
  • operationName the name given to the child context, when it has one

A run_in_child_context body, a map item, a parallel branch, and a wait_for_callback body each run in a child context. A map item that you do not name reports map-item-<index>, and such a parallel branch reports parallel-branch-<index>. Inside a child context, a step's records carry the step's operationId, operationName, and attempt in place of the child's.

  • operationId the deterministic operation ID of the child context operation
  • operationName the name given to the child context, when you provide one

Operation context

The following operation contexts support the logger:

All DurableContext fields, plus:

  • operationId the unique ID of the operation
  • operationName the operation name, when you provide one
  • attempt the 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)
}

A record written in a step body, a wait_for_condition check, or a wait_for_callback submitter carries the operation's operationId, its operationName when it has one, and attempt. The SDK enters the operation's span around the body, so every tracing macro there adds these fields. attempt counts from 1 and grows on each retry of a step and on each check of a condition. The submitter's records carry the operationName of the wait_for_callback, when it has one.

use aws_durable_execution_sdk as durable;

async fn handler(
    _event: serde_json::Value,
    ctx: durable::DurableContext,
) -> Result<(), durable::BoxError> {
    ctx.step(|_| async {
        // Records in the step body include operationId and attempt.
        tracing::info!("Running step");
        Ok("done".to_owned())
    })
    .name("process")
    .await?;
    Ok(())
}

#[tokio::main]
async fn main() -> Result<(), lambda_runtime::Error> {
    durable::init_default_subscriber()?;
    durable::run(handler).await
}

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:

durable.Start(handler, durable.WithReplayLogMode(durable.ReplayLogModeEmit))

To change the mode from inside the handler, call ConfigureLogging. See Configure logger.

use aws_durable_execution_sdk as durable;

async fn handler(
    _event: serde_json::Value,
    ctx: durable::DurableContext,
) -> Result<(), durable::BoxError> {
    // The default subscriber suppresses duplicate logs during replay.
    tracing::info!("Step 1 starting");

    ctx.step(|_| async { Ok("result".to_owned()) })
        .name("step-1")
        .await?;

    tracing::info!("Step 1 complete");
    Ok(())
}

#[tokio::main]
async fn main() -> Result<(), lambda_runtime::Error> {
    durable::init_default_subscriber()?;
    durable::run(handler).await
}

The subscriber that durable::init_default_subscriber() installs filters its output with durable::ReplayFilterLayer, which drops the records written while the SDK replays. To emit logs on every replay, install a subscriber without that filter in main, such as lambda_runtime::tracing::init_default_subscriber(). It reads the same environment variables. Its JSON record puts the message and fields under fields and the span fields under span and spans.

use aws_durable_execution_sdk as durable;

async fn handler(
    _event: serde_json::Value,
    ctx: durable::DurableContext,
) -> Result<(), durable::BoxError> {
    tracing::info!("This logs on every replay");

    ctx.step(|_| async { Ok("result".to_owned()) })
        .name("step-1")
        .await?;
    Ok(())
}

#[tokio::main]
async fn main() -> Result<(), lambda_runtime::Error> {
    // Install a subscriber without the replay filter to emit logs on every replay.
    lambda_runtime::tracing::init_default_subscriber();
    durable::run(handler).await
}
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.

Pass an object that implements DurableLogger to configureLogger. To include execution metadata and plugin fields, the logger must also implement configureDurableLoggingContext. Store the supplied context and read getDurableLogData() and getLogEnrichment?.() on each log call. createPowertoolsLogger handles this for a Powertools logger:

import {
  createPowertoolsLogger,
  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: createPowertoolsLogger(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))
}

Install any tracing subscriber in main. lambda_runtime::tracing::subscriber re-exports tracing-subscriber, the crate that builds subscribers from layers. To keep replay suppression, set durable::ReplayFilterLayer as the filter of the layer that writes records. The SDK creates its spans at INFO, so every filter must keep INFO spans enabled. A filter above INFO hides the spans from ReplayFilterLayer, and the records then repeat on every replay and lose the SDK's fields. The SDK's span fields reach every layer. The stock fmt::layer().json() writes the message and fields under fields and the span fields under span and spans.

use aws_durable_execution_sdk as durable;
use lambda_runtime::tracing::subscriber;
use subscriber::Layer as _;
use subscriber::layer::SubscriberExt as _;
use subscriber::util::SubscriberInitExt as _;

async fn handler(
    _event: serde_json::Value,
    _ctx: durable::DurableContext,
) -> Result<(), durable::BoxError> {
    tracing::info!("Using a custom subscriber");
    Ok(())
}

#[tokio::main]
async fn main() -> Result<(), lambda_runtime::Error> {
    subscriber::registry()
        .with(
            subscriber::fmt::layer()
                .json()
                .with_filter(durable::ReplayFilterLayer::new()),
        )
        .try_init()?;
    durable::run(handler).await
}

Any Microsoft.Extensions.Logging.ILogger works. Pass it as the CustomLogger on LoggerConfig to context.ConfigureLogger.

context.ConfigureLogger(new LoggerConfig { CustomLogger = myLogger });

Configure logger

Configure the logger on the handler's DurableContext.

context.configureLogger(config: LoggerConfig): void

LoggerConfig

interface LoggerConfig<Logger extends DurableLogger> {
  customLogger?: Logger;
  modeAware?: boolean;
}

LoggerConfig parameters:

  • customLogger (optional) A DurableLogger implementation to use instead of the default console logger.
  • modeAware (optional) When true (default), the SDK suppresses logs during replay. Set to false to emit logs on every replay.

Set the logger on the handler's DurableContext.

context.set_logger(new_logger: LoggerInterface) -> None

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) When true (default), the SDK suppresses logs during replay. Use LoggerConfig.withReplayLogging() to set this to false.
  • oldKeyNames (boolean) When false (default), the SDK uses the Java 2.x MDC keys executionArn, operationId, and operationName. Set it to true temporarily to emit the 1.x keys durableExecutionArn, contextId, and contextName while migrating log queries and dashboards.
DurableConfig.builder()
    .withLoggerConfig(LoggerConfig.withReplayLogging())
    .build();

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 behind Context.Logger and StepContext.Logger. The SDK attaches its execution and operation attributes to it and wraps it with replay suppression. nil keeps 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.

Install the default subscriber once in main, before durable::run.

pub fn init_default_subscriber() -> Result<(), BoxError>;

// ...

pub fn init_default_subscriber_with_writer<W>(writer: W) -> Result<(), BoxError>
where
    W: for<'w> lambda_runtime::tracing::subscriber::fmt::MakeWriter<'w> + Send + Sync + 'static;

Parameters:

  • writer (init_default_subscriber_with_writer only) Any MakeWriter, such as a non-blocking writer. init_default_subscriber() writes to stdout.

Returns: Ok(()) when the subscriber is installed.

Errors: the try_init error when a global subscriber is already installed, for example by an earlier call.

Environment variables:

  • RUST_LOG The EnvFilter directives, such as info,my_crate=debug. A valid directive here sets the level.
  • AWS_LAMBDA_LOG_LEVEL The level when RUST_LOG is unset or holds only invalid directives. When it is unset too, the level is INFO.
  • AWS_LAMBDA_LOG_FORMAT JSON in any case selects JSON output. Any other value selects text.

Both functions read these variables when you call them. The subscriber applies to the whole process.

Configure the logger on the handler's IDurableContext.

void IDurableContext.ConfigureLogger(LoggerConfig config);

LoggerConfig

public sealed class LoggerConfig
{
    public ILogger? CustomLogger { get; init; }  // null = keep current logger
    public bool ModeAware { get; init; } = true;
}

LoggerConfig parameters:

  • CustomLogger (optional) An ILogger to use instead of the SDK default. When null, the durable context keeps its existing inner logger.
  • ModeAware (optional) When true (default), the SDK suppresses logs during replay. Set to false to 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 logger interface is the tracing Subscriber trait. Build a subscriber from tracing-subscriber layers, or use any crate that provides one. durable::ReplayFilterLayer is a per-layer filter that drops the records written while the SDK replays. Create it with ReplayFilterLayer::new() or Default. In the declaration, subscriber is the tracing-subscriber crate.

#[derive(Debug, Clone, Default)]
#[non_exhaustive]
pub struct ReplayFilterLayer;

impl ReplayFilterLayer {
    pub fn new() -> Self;
    // ...
}

impl<S> subscriber::layer::Filter<S> for ReplayFilterLayer
where
    S: tracing::Subscriber + for<'lookup> subscriber::registry::LookupSpan<'lookup>,
{
    // ...
}

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 structured logging that you can use with the SDK.

The Powertools for AWS Lambda (TypeScript) Logger works with createPowertoolsLogger. Pass the wrapped logger via configureLogger so each entry includes execution metadata and fields added by plugins, such as OpenTelemetry trace IDs. See log correlation for an example with the OpenTelemetry plugin.

import { Logger } from "@aws-lambda-powertools/logger";
import {
  createPowertoolsLogger,
  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: createPowertoolsLogger(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.

Powertools for AWS Lambda offers no Rust library. For structured JSON output, call durable::init_default_subscriber(), or install any tracing subscriber with durable::ReplayFilterLayer. 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");
    }
}

See also