From dabee4e2b1369577532f7dda37de241a6b019549 Mon Sep 17 00:00:00 2001 From: Pooya Paridel Date: Wed, 12 Nov 2025 10:49:55 -0800 Subject: [PATCH 1/2] feat(sdk): add configureLogger method with modeAware option Replace setCustomLogger() with configureLogger() that accepts LoggerConfig object with customLogger and modeAware options. The modeAware flag controls whether logs are suppressed during replay mode (default: true). BREAKING CHANGE: Removed setCustomLogger() method in favor of configureLogger(). This is acceptable as the SDK has not been launched yet and there are no existing users. Changes: - Add configureLogger(config: LoggerConfig) method to DurableContext - Add LoggerConfig interface with customLogger and modeAware properties - Update createModeAwareLogger to accept modeAwareEnabled parameter - Change default modeAware behavior to true (suppress logs during replay) - Fix replay mode switching for backend-completed operations (wait, invoke, createCallback, waitForCallback) by calling checkAndUpdateReplayMode() after completion - Update all tests to reflect new default behavior and mode switching fix - Add logger-test examples with file-based logging (local-only tests) - Update documentation: README, API_SPECIFICATION.md with new API - Add TSDoc comments for Logger types and configureLogger method - Remove old logger-example with outdated information --- .../logger-example/logger-example.test.ts | 25 -- .../examples/logger-example/logger-example.ts | 70 ------ .../logger-after-callback.test.ts | 132 +++++++++++ .../after-callback/logger-after-callback.ts | 70 ++++++ .../after-wait/logger-after-wait.test.ts | 60 +++++ .../after-wait/logger-after-wait.ts | 65 ++++++ .../aws-durable-execution-sdk-js/README.md | 31 ++- .../durable-context/durable-context.ts | 50 +++- .../durable-context.unit.test.ts | 4 +- .../durable-context/logger-mode-aware.test.ts | 219 ++++++++++++++++++ .../durable-context/logger-property.test.ts | 18 +- .../src/documents/API_SPECIFICATION.md | 51 +++- .../src/types/durable-context.ts | 21 +- .../src/types/logger.ts | 19 ++ .../utils/logger/mode-aware-logger.test.ts | 13 +- .../src/utils/logger/mode-aware-logger.ts | 4 +- 16 files changed, 719 insertions(+), 133 deletions(-) delete mode 100644 packages/aws-durable-execution-sdk-js-examples/src/examples/logger-example/logger-example.test.ts delete mode 100644 packages/aws-durable-execution-sdk-js-examples/src/examples/logger-example/logger-example.ts create mode 100644 packages/aws-durable-execution-sdk-js-examples/src/examples/logger-test/after-callback/logger-after-callback.test.ts create mode 100644 packages/aws-durable-execution-sdk-js-examples/src/examples/logger-test/after-callback/logger-after-callback.ts create mode 100644 packages/aws-durable-execution-sdk-js-examples/src/examples/logger-test/after-wait/logger-after-wait.test.ts create mode 100644 packages/aws-durable-execution-sdk-js-examples/src/examples/logger-test/after-wait/logger-after-wait.ts create mode 100644 packages/aws-durable-execution-sdk-js/src/context/durable-context/logger-mode-aware.test.ts diff --git a/packages/aws-durable-execution-sdk-js-examples/src/examples/logger-example/logger-example.test.ts b/packages/aws-durable-execution-sdk-js-examples/src/examples/logger-example/logger-example.test.ts deleted file mode 100644 index e3b482db..00000000 --- a/packages/aws-durable-execution-sdk-js-examples/src/examples/logger-example/logger-example.test.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { loggerExample } from "./logger-example"; -import { withDurableExecution } from "@aws/durable-execution-sdk-js"; -import { LocalDurableTestRunner } from "@aws/durable-execution-sdk-js-testing"; - -const handler = withDurableExecution(loggerExample); - -describe("logger-example test (local)", () => { - beforeAll(() => LocalDurableTestRunner.setupTestEnvironment()); - afterAll(() => LocalDurableTestRunner.teardownTestEnvironment()); - - const runner = new LocalDurableTestRunner({ - handlerFunction: handler, - skipTime: true, - }); - - beforeEach(() => { - runner.reset(); - }); - - it("should execute successfully with logger", async () => { - const execution = await runner.run(); - - expect(execution.getResult()).toEqual("processed-child-processed"); - }); -}); diff --git a/packages/aws-durable-execution-sdk-js-examples/src/examples/logger-example/logger-example.ts b/packages/aws-durable-execution-sdk-js-examples/src/examples/logger-example/logger-example.ts deleted file mode 100644 index 8d74e13c..00000000 --- a/packages/aws-durable-execution-sdk-js-examples/src/examples/logger-example/logger-example.ts +++ /dev/null @@ -1,70 +0,0 @@ -/** - * Example demonstrating logger usage in DurableContext - * - * This example shows: - * 1. How to access the logger from DurableContext - * 2. How child contexts inherit the parent's logger - * 3. How step contexts get enriched loggers - * 4. Logger only logs in ExecutionMode (not during replay) - * 5. Context logger enrichment with execution ARN and step ID - * - * Logger Enrichment: - * - Top-level DurableContext: Enriched with execution_arn (no step_id) - * - Child context: Enriched with execution_arn, step_id="" - * - Step context: Enriched with execution_arn, step_id="", attempt - * - * Note: The DurableContext logger automatically checks the execution mode - * and only logs when in ExecutionMode. During replay (ReplayMode or - * ReplaySucceededContext), logging is suppressed to avoid duplicate logs. - */ - -import { DurableContext } from "@aws/durable-execution-sdk-js"; - -export async function loggerExample( - event: any, - context: DurableContext, -): Promise { - // Top-level context logger: no step_id field - context.logger.info("Starting workflow", { eventId: event.id }); - // Logs: { execution_arn: "...", level: "info", message: "Starting workflow", data: { eventId: ... }, timestamp: "..." } - - // Logger in steps - gets enriched with step ID and attempt number - const result1 = await context.step("process-data", async (stepCtx) => { - // Step context has an enriched logger with step ID and attempt number - stepCtx.logger.info("Processing data in step"); - // Logs: { execution_arn: "...", step_id: "1", attempt: 0, level: "info", message: "Processing data in step", timestamp: "..." } - return "processed"; - }); - - context.logger.info("Step 1 completed", { result: result1 }); - - // Child contexts inherit the parent's logger and have their own step ID - const result2 = await context.runInChildContext( - "child-workflow", - async (childCtx) => { - // Child context logger has step_id populated with child context ID - childCtx.logger.info("Running in child context"); - // Logs: { execution_arn: "...", step_id: "2", level: "info", message: "Running in child context", timestamp: "..." } - - const childResult = await childCtx.step("child-step", async (stepCtx) => { - // Step in child context has nested step ID - stepCtx.logger.info("Processing in child step"); - // Logs: { execution_arn: "...", step_id: "2-1", attempt: 0, level: "info", message: "Processing in child step", timestamp: "..." } - return "child-processed"; - }); - - childCtx.logger.info("Child workflow completed", { - result: childResult, - }); - - return childResult; - }, - ); - - context.logger.info("Workflow completed", { - result1, - result2, - }); - - return `${result1}-${result2}`; -} diff --git a/packages/aws-durable-execution-sdk-js-examples/src/examples/logger-test/after-callback/logger-after-callback.test.ts b/packages/aws-durable-execution-sdk-js-examples/src/examples/logger-test/after-callback/logger-after-callback.test.ts new file mode 100644 index 00000000..7e71f91a --- /dev/null +++ b/packages/aws-durable-execution-sdk-js-examples/src/examples/logger-test/after-callback/logger-after-callback.test.ts @@ -0,0 +1,132 @@ +import { handler } from "./logger-after-callback"; +import { createTests } from "../../../utils/test-helper"; +import { + InvocationType, + WaitingOperationStatus, +} from "@aws/durable-execution-sdk-js-testing"; +import * as fs from "fs"; +import * as path from "path"; +import * as os from "os"; +import { randomUUID } from "crypto"; + +createTests({ + name: "logger-after-callback", + functionName: "logger-after-callback", + handler, + invocationType: InvocationType.Event, + tests: (runner, isCloud) => { + if (!isCloud) { + it("should log correctly with modeAware=true", async () => { + const logFilePath = path.join( + os.tmpdir(), + `logger-test-${randomUUID()}.log`, + ); + + if (fs.existsSync(logFilePath)) { + fs.unlinkSync(logFilePath); + } + + try { + const executionPromise = runner.run({ + payload: { logFilePath, modeAware: true }, + }); + + const callbackOp = runner.getOperationByIndex(0); + await callbackOp.waitForData(WaitingOperationStatus.STARTED); + await callbackOp.sendCallbackSuccess("test-result"); + + const execution = await executionPromise; + + const result = execution.getResult() as any; + expect(result.message).toBe("Success"); + expect(result.callbackId).toBeDefined(); + expect(result.result).toBe("test-result"); + + const logContent = fs.readFileSync(logFilePath, "utf-8"); + const logLines = logContent + .trim() + .split("\n") + .map((line) => JSON.parse(line)); + + const beforeCallbackLogs = logLines.filter( + (log) => log.message === "Before createCallback", + ); + const afterCallbackLogs = logLines.filter( + (log) => log.message === "After createCallback", + ); + + // With modeAware: true: + // - "Before createCallback" appears once (execution mode only, suppressed during replay) + // - "After createCallback" appears once (after callback resolves, in execution mode) + expect(beforeCallbackLogs.length).toBe(1); + expect(afterCallbackLogs.length).toBe(1); + } finally { + if (fs.existsSync(logFilePath)) { + fs.unlinkSync(logFilePath); + } + } + }); + + it("should log correctly with modeAware=false", async () => { + const logFilePath = path.join( + os.tmpdir(), + `logger-test-${randomUUID()}.log`, + ); + + if (fs.existsSync(logFilePath)) { + fs.unlinkSync(logFilePath); + } + + try { + const executionPromise = runner.run({ + payload: { logFilePath, modeAware: false }, + }); + + const callbackOp = runner.getOperationByIndex(0); + await callbackOp.waitForData(WaitingOperationStatus.STARTED); + await callbackOp.sendCallbackSuccess("test-result"); + + const execution = await executionPromise; + + const result = execution.getResult() as any; + expect(result.message).toBe("Success"); + + const logContent = fs.readFileSync(logFilePath, "utf-8"); + const logLines = logContent + .trim() + .split("\n") + .map((line) => JSON.parse(line)); + + const beforeCallbackLogs = logLines.filter( + (log) => log.message === "Before createCallback", + ); + const afterCallbackLogs = logLines.filter( + (log) => log.message === "After createCallback", + ); + + // With modeAware: false: + // - "Before createCallback" appears twice (execution + replay) + // - "After createCallback" appears once (after callback resolves) + expect(beforeCallbackLogs.length).toBe(2); + expect(afterCallbackLogs.length).toBe(1); + } finally { + if (fs.existsSync(logFilePath)) { + fs.unlinkSync(logFilePath); + } + } + }); + } + + it("should execute successfully", async () => { + const executionPromise = runner.run(); + + const callbackOp = runner.getOperationByIndex(0); + await callbackOp.waitForData(WaitingOperationStatus.STARTED); + await callbackOp.sendCallbackSuccess("test-result"); + + const execution = await executionPromise; + const result = execution.getResult() as any; + expect(result.message).toBe("Success"); + }); + }, +}); diff --git a/packages/aws-durable-execution-sdk-js-examples/src/examples/logger-test/after-callback/logger-after-callback.ts b/packages/aws-durable-execution-sdk-js-examples/src/examples/logger-test/after-callback/logger-after-callback.ts new file mode 100644 index 00000000..8d1dd4bf --- /dev/null +++ b/packages/aws-durable-execution-sdk-js-examples/src/examples/logger-test/after-callback/logger-after-callback.ts @@ -0,0 +1,70 @@ +import { withDurableExecution, Logger } from "@aws/durable-execution-sdk-js"; +import { ExampleConfig } from "../../../types"; +import * as fs from "fs"; + +export const config: ExampleConfig = { + name: "Logger After Callback", + description: "Test logger mode switching after createCallback operation", +}; + +interface LoggerTestEvent { + logFilePath?: string; + modeAware?: boolean; +} + +export const handler = withDurableExecution( + async (event: LoggerTestEvent, context) => { + if (event.logFilePath) { + const fileLogger: Logger = { + log: (level, message, data) => { + fs.appendFileSync( + event.logFilePath!, + JSON.stringify({ level, message, data }) + "\n", + ); + }, + info: (message, data) => { + fs.appendFileSync( + event.logFilePath!, + JSON.stringify({ level: "info", message, data }) + "\n", + ); + }, + error: (message, error, data) => { + fs.appendFileSync( + event.logFilePath!, + JSON.stringify({ level: "error", message, error, data }) + "\n", + ); + }, + warn: (message, data) => { + fs.appendFileSync( + event.logFilePath!, + JSON.stringify({ level: "warn", message, data }) + "\n", + ); + }, + debug: (message, data) => { + fs.appendFileSync( + event.logFilePath!, + JSON.stringify({ level: "debug", message, data }) + "\n", + ); + }, + }; + + context.configureLogger({ + customLogger: fileLogger, + modeAware: event.modeAware ?? true, + }); + } else { + context.configureLogger({ modeAware: event.modeAware ?? true }); + } + + context.logger.info("Before createCallback"); + + const [callbackPromise, callbackId] = + await context.createCallback(); + + const result = await callbackPromise; + + context.logger.info("After createCallback"); + + return { message: "Success", callbackId, result }; + }, +); diff --git a/packages/aws-durable-execution-sdk-js-examples/src/examples/logger-test/after-wait/logger-after-wait.test.ts b/packages/aws-durable-execution-sdk-js-examples/src/examples/logger-test/after-wait/logger-after-wait.test.ts new file mode 100644 index 00000000..db2a12a7 --- /dev/null +++ b/packages/aws-durable-execution-sdk-js-examples/src/examples/logger-test/after-wait/logger-after-wait.test.ts @@ -0,0 +1,60 @@ +import { handler } from "./logger-after-wait"; +import { createTests } from "../../../utils/test-helper"; +import * as fs from "fs"; +import * as path from "path"; +import * as os from "os"; +import { randomUUID } from "crypto"; + +createTests({ + name: "logger-after-wait", + functionName: "logger-after-wait", + handler, + tests: (runner, isCloud) => { + if (!isCloud) { + it("should log after wait in execution mode with modeAware=true", async () => { + const logFilePath = path.join( + os.tmpdir(), + `logger-test-${randomUUID()}.log`, + ); + + if (fs.existsSync(logFilePath)) { + fs.unlinkSync(logFilePath); + } + + try { + const execution = await runner.run({ + payload: { logFilePath, modeAware: true }, + }); + + expect(execution.getResult()).toEqual({ message: "Success" }); + + const logContent = fs.readFileSync(logFilePath, "utf-8"); + const logLines = logContent + .trim() + .split("\n") + .map((line) => JSON.parse(line)); + + const beforeWaitLogs = logLines.filter( + (log) => log.message === "Before wait", + ); + const afterWaitLogs = logLines.filter( + (log) => log.message === "After wait", + ); + + // With modeAware: true, both logs appear once (execution mode only) + expect(beforeWaitLogs.length).toBe(1); + expect(afterWaitLogs.length).toBe(1); + } finally { + if (fs.existsSync(logFilePath)) { + fs.unlinkSync(logFilePath); + } + } + }); + } + + it("should execute successfully", async () => { + const execution = await runner.run(); + expect(execution.getResult()).toEqual({ message: "Success" }); + }); + }, +}); diff --git a/packages/aws-durable-execution-sdk-js-examples/src/examples/logger-test/after-wait/logger-after-wait.ts b/packages/aws-durable-execution-sdk-js-examples/src/examples/logger-test/after-wait/logger-after-wait.ts new file mode 100644 index 00000000..32a3e921 --- /dev/null +++ b/packages/aws-durable-execution-sdk-js-examples/src/examples/logger-test/after-wait/logger-after-wait.ts @@ -0,0 +1,65 @@ +import { withDurableExecution, Logger } from "@aws/durable-execution-sdk-js"; +import { ExampleConfig } from "../../../types"; +import * as fs from "fs"; + +export const config: ExampleConfig = { + name: "Logger After Wait", + description: "Test logger mode switching after wait operation", +}; + +interface LoggerTestEvent { + logFilePath?: string; + modeAware?: boolean; +} + +export const handler = withDurableExecution( + async (event: LoggerTestEvent, context) => { + if (event.logFilePath) { + const fileLogger: Logger = { + log: (level, message, data) => { + fs.appendFileSync( + event.logFilePath!, + JSON.stringify({ level, message, data }) + "\n", + ); + }, + info: (message, data) => { + fs.appendFileSync( + event.logFilePath!, + JSON.stringify({ level: "info", message, data }) + "\n", + ); + }, + error: (message, error, data) => { + fs.appendFileSync( + event.logFilePath!, + JSON.stringify({ level: "error", message, error, data }) + "\n", + ); + }, + warn: (message, data) => { + fs.appendFileSync( + event.logFilePath!, + JSON.stringify({ level: "warn", message, data }) + "\n", + ); + }, + debug: (message, data) => { + fs.appendFileSync( + event.logFilePath!, + JSON.stringify({ level: "debug", message, data }) + "\n", + ); + }, + }; + + context.configureLogger({ + customLogger: fileLogger, + modeAware: event.modeAware ?? true, + }); + } else { + context.configureLogger({ modeAware: event.modeAware ?? true }); + } + + context.logger.info("Before wait"); + await context.wait({ seconds: 2 }); + context.logger.info("After wait"); + + return { message: "Success" }; + }, +); diff --git a/packages/aws-durable-execution-sdk-js/README.md b/packages/aws-durable-execution-sdk-js/README.md index b190a3ee..e94c2e06 100644 --- a/packages/aws-durable-execution-sdk-js/README.md +++ b/packages/aws-durable-execution-sdk-js/README.md @@ -395,16 +395,33 @@ const handler = async (event: any, context: DurableContext) => { Custom logger: ```typescript -context.setCustomLogger({ - log: (level, message, data, error) => - console.log(`[${level}] ${message}`, data), - error: (message, error, data) => console.error(message, error, data), - warn: (message, data) => console.warn(message, data), - info: (message, data) => console.info(message, data), - debug: (message, data) => console.debug(message, data), +context.configureLogger({ + customLogger: { + log: (level, message, data, error) => + console.log(`[${level}] ${message}`, data), + error: (message, error, data) => console.error(message, error, data), + warn: (message, data) => console.warn(message, data), + info: (message, data) => console.info(message, data), + debug: (message, data) => console.debug(message, data), + }, + modeAware: false, // Optional: show logs during replay (default: true) }); ``` +### Migration from setCustomLogger() + +If you were using the deprecated `setCustomLogger()` method, update your code: + +```typescript +// Before +context.setCustomLogger(myLogger); + +// After +context.configureLogger({ customLogger: myLogger }); +``` + +The new `configureLogger()` method provides additional control over logging behavior with the `modeAware` option. + ## Testing Locally Run durable functions locally for development: diff --git a/packages/aws-durable-execution-sdk-js/src/context/durable-context/durable-context.ts b/packages/aws-durable-execution-sdk-js/src/context/durable-context/durable-context.ts index 478d7791..3a5c31c2 100644 --- a/packages/aws-durable-execution-sdk-js/src/context/durable-context/durable-context.ts +++ b/packages/aws-durable-execution-sdk-js/src/context/durable-context/durable-context.ts @@ -21,6 +21,7 @@ import { ConcurrentExecutor, ConcurrencyConfig, Logger, + LoggerConfig, InvokeConfig, DurableExecutionMode, BatchResult, @@ -50,6 +51,7 @@ class DurableContextImpl implements DurableContext { private _stepPrefix?: string; private _stepCounter: number = 0; private contextLogger: Logger | null; + private modeAwareLoggingEnabled: boolean = true; private runningOperations = new Set(); private operationsEmitter = new EventEmitter(); private checkpoint: ReturnType; @@ -91,6 +93,7 @@ class DurableContextImpl implements DurableContext { return createModeAwareLogger( this.durableExecutionMode, this.createContextLogger, + this.modeAwareLoggingEnabled, this._stepPrefix, ); } @@ -235,7 +238,7 @@ class DurableContextImpl implements DurableContext { "invoke", this.executionContext.terminationManager, ); - return this.withModeManagement(() => { + return this.withModeManagement(async () => { const invokeHandler = createInvokeHandler( this.executionContext, this.checkpoint, @@ -254,7 +257,9 @@ class DurableContextImpl implements DurableContext { ); // Prevent unhandled promise rejections promise?.catch(() => {}); - return promise; + const result = await promise; + this.checkAndUpdateReplayMode(); + return result; }); } @@ -294,7 +299,7 @@ class DurableContextImpl implements DurableContext { "wait", this.executionContext.terminationManager, ); - return this.withModeManagement(() => { + return this.withModeManagement(async () => { const waitHandler = createWaitHandler( this.executionContext, this.checkpoint, @@ -309,12 +314,33 @@ class DurableContextImpl implements DurableContext { : waitHandler(nameOrDuration); // Prevent unhandled promise rejections promise?.catch(() => {}); - return promise; + await promise; + this.checkAndUpdateReplayMode(); }); } - setCustomLogger(logger: Logger): void { - this.contextLogger = logger; + /** + * Configure logger behavior for this context + * + * This method allows partial configuration - only the properties provided will be updated. + * For example, calling configureLogger(\{ modeAware: false \}) will only change the modeAware + * setting without affecting any previously configured custom logger. + * + * @param config - Logger configuration options including customLogger and modeAware settings (default: modeAware=true) + * @example + * // Set custom logger and enable mode-aware logging + * context.configureLogger(\{ customLogger: myLogger, modeAware: true \}); + * + * // Later, disable mode-aware logging without changing the custom logger + * context.configureLogger(\{ modeAware: false \}); + */ + configureLogger(config: LoggerConfig): void { + if (config.customLogger !== undefined) { + this.contextLogger = config.customLogger; + } + if (config.modeAware !== undefined) { + this.modeAwareLoggingEnabled = config.modeAware; + } } createCallback( @@ -326,7 +352,7 @@ class DurableContextImpl implements DurableContext { "createCallback", this.executionContext.terminationManager, ); - return this.withModeManagement(() => { + return this.withModeManagement(async () => { const callbackFactory = createCallbackFactory( this.executionContext, this.checkpoint, @@ -338,7 +364,9 @@ class DurableContextImpl implements DurableContext { const promise = callbackFactory(nameOrConfig, maybeConfig); // Prevent unhandled promise rejections promise?.catch(() => {}); - return promise; + const result = await promise; + this.checkAndUpdateReplayMode(); + return result; }); } @@ -352,7 +380,7 @@ class DurableContextImpl implements DurableContext { "waitForCallback", this.executionContext.terminationManager, ); - return this.withModeManagement(() => { + return this.withModeManagement(async () => { const waitForCallbackHandler = createWaitForCallbackHandler( this.executionContext, this.runInChildContext.bind(this), @@ -364,7 +392,9 @@ class DurableContextImpl implements DurableContext { ); // Prevent unhandled promise rejections promise?.catch(() => {}); - return promise; + const result = await promise; + this.checkAndUpdateReplayMode(); + return result; }); } diff --git a/packages/aws-durable-execution-sdk-js/src/context/durable-context/durable-context.unit.test.ts b/packages/aws-durable-execution-sdk-js/src/context/durable-context/durable-context.unit.test.ts index 669238af..8a0abe5f 100644 --- a/packages/aws-durable-execution-sdk-js/src/context/durable-context/durable-context.unit.test.ts +++ b/packages/aws-durable-execution-sdk-js/src/context/durable-context/durable-context.unit.test.ts @@ -99,7 +99,7 @@ describe("DurableContext", () => { expect(context.executeConcurrently).toBeDefined(); expect(context.promise).toBeDefined(); expect(context.logger).toBeDefined(); - expect(context.setCustomLogger).toBeDefined(); + expect(context.configureLogger).toBeDefined(); }); it("should expose lambdaContext", () => { @@ -501,7 +501,7 @@ describe("DurableContext", () => { debug: jest.fn(), }; - expect(() => context.setCustomLogger(customLogger)).not.toThrow(); + expect(() => context.configureLogger({ customLogger })).not.toThrow(); }); }); diff --git a/packages/aws-durable-execution-sdk-js/src/context/durable-context/logger-mode-aware.test.ts b/packages/aws-durable-execution-sdk-js/src/context/durable-context/logger-mode-aware.test.ts new file mode 100644 index 00000000..f4f6f5af --- /dev/null +++ b/packages/aws-durable-execution-sdk-js/src/context/durable-context/logger-mode-aware.test.ts @@ -0,0 +1,219 @@ +import { createDurableContext } from "./durable-context"; +import { DurableExecutionMode, ExecutionContext, Logger } from "../../types"; +import { Context } from "aws-lambda"; + +describe("DurableContext logger modeAware configuration", () => { + const mockExecutionContext: ExecutionContext = { + _stepData: {}, + durableExecutionArn: "test-arn", + terminationManager: { + terminate: jest.fn(), + getTerminationPromise: jest.fn().mockResolvedValue({ reason: "test" }), + }, + getStepData: jest.fn(), + state: { + getStepData: jest.fn(), + checkpoint: jest.fn(), + }, + } as any; + + const mockParentContext: Context = { + functionName: "test-function", + functionVersion: "1", + invokedFunctionArn: "test-arn", + memoryLimitInMB: "128", + awsRequestId: "test-request-id", + logGroupName: "test-log-group", + logStreamName: "test-log-stream", + getRemainingTimeInMillis: () => 1000, + done: () => {}, + fail: () => {}, + succeed: () => {}, + callbackWaitsForEmptyEventLoop: true, + }; + + test("should suppress logs during replay when modeAware is true (default)", () => { + const customLogger: Logger = { + log: jest.fn(), + info: jest.fn(), + error: jest.fn(), + warn: jest.fn(), + debug: jest.fn(), + }; + + const context = createDurableContext( + mockExecutionContext, + mockParentContext, + DurableExecutionMode.ReplayMode, + ); + context.configureLogger({ customLogger }); + + context.logger.info("replay message"); + expect(customLogger.info).not.toHaveBeenCalled(); + }); + + test("should log during replay when modeAware is false", () => { + const customLogger: Logger = { + log: jest.fn(), + info: jest.fn(), + error: jest.fn(), + warn: jest.fn(), + debug: jest.fn(), + }; + + const context = createDurableContext( + mockExecutionContext, + mockParentContext, + DurableExecutionMode.ReplayMode, + ); + context.configureLogger({ customLogger, modeAware: false }); + + context.logger.info("replay message"); + expect(customLogger.info).toHaveBeenCalledWith( + "replay message", + expect.objectContaining({ + level: "info", + message: "replay message", + execution_arn: "test-arn", + }), + ); + }); + + test("should always log during execution mode regardless of modeAware", () => { + const customLogger: Logger = { + log: jest.fn(), + info: jest.fn(), + error: jest.fn(), + warn: jest.fn(), + debug: jest.fn(), + }; + + const context = createDurableContext( + mockExecutionContext, + mockParentContext, + DurableExecutionMode.ExecutionMode, + ); + context.configureLogger({ customLogger, modeAware: true }); + + context.logger.info("execution message"); + expect(customLogger.info).toHaveBeenCalled(); + }); + + test("should allow toggling modeAware at runtime", () => { + const customLogger: Logger = { + log: jest.fn(), + info: jest.fn(), + error: jest.fn(), + warn: jest.fn(), + debug: jest.fn(), + }; + + const context = createDurableContext( + mockExecutionContext, + mockParentContext, + DurableExecutionMode.ReplayMode, + ); + context.configureLogger({ customLogger }); + + // Default: modeAware = true, should not log during replay + context.logger.info("message1"); + expect(customLogger.info).not.toHaveBeenCalled(); + + // Disable modeAware: should log during replay + context.configureLogger({ modeAware: false }); + context.logger.info("message2"); + expect(customLogger.info).toHaveBeenCalledTimes(1); + + // Re-enable modeAware: should not log during replay again + context.configureLogger({ modeAware: true }); + context.logger.info("message3"); + expect(customLogger.info).toHaveBeenCalledTimes(1); // Still 1, no new call + }); + + test("should use default modeAware=true when called with empty config", () => { + const context = createDurableContext( + mockExecutionContext, + mockParentContext, + DurableExecutionMode.ReplayMode, + ); + context.configureLogger({}); + + // With default modeAware=true, should not log during replay + context.logger.info("replay message"); + + // Default logger logs to console, but in replay mode with modeAware=true it should be suppressed + // We can't easily test console output, but we can verify the context was configured + expect(context.logger).toBeDefined(); + }); + + test("should handle multiple partial configurations correctly", () => { + const customLogger1: Logger = { + log: jest.fn(), + info: jest.fn(), + error: jest.fn(), + warn: jest.fn(), + debug: jest.fn(), + }; + + const customLogger2: Logger = { + log: jest.fn(), + info: jest.fn(), + error: jest.fn(), + warn: jest.fn(), + debug: jest.fn(), + }; + + const context = createDurableContext( + mockExecutionContext, + mockParentContext, + DurableExecutionMode.ReplayMode, + ); + + // First: set custom logger only + context.configureLogger({ customLogger: customLogger1 }); + context.logger.info("message1"); + expect(customLogger1.info).not.toHaveBeenCalled(); // modeAware=true by default + + // Second: change modeAware only (should keep customLogger1) + context.configureLogger({ modeAware: false }); + context.logger.info("message2"); + expect(customLogger1.info).toHaveBeenCalledTimes(1); // Now logs with customLogger1 + + // Third: change custom logger only (should keep modeAware=false) + context.configureLogger({ customLogger: customLogger2 }); + context.logger.info("message3"); + expect(customLogger1.info).toHaveBeenCalledTimes(1); // No more calls to logger1 + expect(customLogger2.info).toHaveBeenCalledTimes(1); // Now uses logger2 + + // Fourth: change modeAware back to true (should keep customLogger2) + context.configureLogger({ modeAware: true }); + context.logger.info("message4"); + expect(customLogger2.info).toHaveBeenCalledTimes(1); // No new calls, suppressed + }); + + test("should preserve settings when called with empty config", () => { + const customLogger: Logger = { + log: jest.fn(), + info: jest.fn(), + error: jest.fn(), + warn: jest.fn(), + debug: jest.fn(), + }; + + const context = createDurableContext( + mockExecutionContext, + mockParentContext, + DurableExecutionMode.ReplayMode, + ); + + // Set custom logger and modeAware=false + context.configureLogger({ customLogger, modeAware: false }); + context.logger.info("message1"); + expect(customLogger.info).toHaveBeenCalledTimes(1); + + // Call with empty config - should preserve both settings + context.configureLogger({}); + context.logger.info("message2"); + expect(customLogger.info).toHaveBeenCalledTimes(2); // Still uses customLogger with modeAware=false + }); +}); diff --git a/packages/aws-durable-execution-sdk-js/src/context/durable-context/logger-property.test.ts b/packages/aws-durable-execution-sdk-js/src/context/durable-context/logger-property.test.ts index ddee719e..730f1e4f 100644 --- a/packages/aws-durable-execution-sdk-js/src/context/durable-context/logger-property.test.ts +++ b/packages/aws-durable-execution-sdk-js/src/context/durable-context/logger-property.test.ts @@ -51,7 +51,7 @@ describe("DurableContext Logger Property", () => { expect(typeof context.logger.debug).toBe("function"); }); - test("DurableContext logger should be customizable via setCustomLogger", () => { + test("DurableContext logger should be customizable via configureLogger", () => { const context = createDurableContext( mockExecutionContext, mockParentContext, @@ -59,7 +59,7 @@ describe("DurableContext Logger Property", () => { ); // Set custom logger - context.setCustomLogger(customLogger); + context.configureLogger({ customLogger }); // Verify it works by calling a method context.logger.info("test message", { data: "test" }); @@ -91,7 +91,7 @@ describe("DurableContext Logger Property", () => { context.logger.info("message1"); // Set custom logger - context.setCustomLogger(customLogger); + context.configureLogger({ customLogger }); // Call logger after setting custom logger context.logger.info("message2"); @@ -117,7 +117,7 @@ describe("DurableContext Logger Property", () => { mockParentContext, DurableExecutionMode.ExecutionMode, ); - contextExecution.setCustomLogger(customLogger); + contextExecution.configureLogger({ customLogger }); contextExecution.logger.info("execution mode message"); expect(customLogger.info).toHaveBeenCalledWith( @@ -134,13 +134,13 @@ describe("DurableContext Logger Property", () => { // Reset mock jest.clearAllMocks(); - // Test in ReplayMode - should NOT log + // Test in ReplayMode - should NOT log when modeAware is true (default) const contextReplay = createDurableContext( mockExecutionContext, mockParentContext, DurableExecutionMode.ReplayMode, ); - contextReplay.setCustomLogger(customLogger); + contextReplay.configureLogger({ customLogger }); contextReplay.logger.info("replay mode message"); expect(customLogger.info).not.toHaveBeenCalled(); @@ -148,13 +148,13 @@ describe("DurableContext Logger Property", () => { // Reset mock jest.clearAllMocks(); - // Test in ReplaySucceededContext - should NOT log + // Test in ReplaySucceededContext - should NOT log when modeAware is true (default) const contextReplaySucceeded = createDurableContext( mockExecutionContext, mockParentContext, DurableExecutionMode.ReplaySucceededContext, ); - contextReplaySucceeded.setCustomLogger(customLogger); + contextReplaySucceeded.configureLogger({ customLogger }); contextReplaySucceeded.logger.info("replay succeeded message"); expect(customLogger.info).not.toHaveBeenCalled(); @@ -168,7 +168,7 @@ describe("DurableContext Logger Property", () => { DurableExecutionMode.ExecutionMode, "1", // stepPrefix for child context ); - childContext.setCustomLogger(customLogger); + childContext.configureLogger({ customLogger }); childContext.logger.info("child message"); diff --git a/packages/aws-durable-execution-sdk-js/src/documents/API_SPECIFICATION.md b/packages/aws-durable-execution-sdk-js/src/documents/API_SPECIFICATION.md index 050f06bf..f0d3a5f0 100644 --- a/packages/aws-durable-execution-sdk-js/src/documents/API_SPECIFICATION.md +++ b/packages/aws-durable-execution-sdk-js/src/documents/API_SPECIFICATION.md @@ -130,7 +130,7 @@ interface DurableContext { }; // Logger customization - setCustomLogger(logger: Logger): void; + configureLogger(config: LoggerConfig): void; } ``` @@ -217,6 +217,55 @@ type ConcurrentExecutor = ( ) => Promise; ``` +### Logger Types + +```typescript +/** + * Generic logger interface for custom logger implementations + * Provides structured logging capabilities for durable execution contexts + */ +export interface Logger { + /** Generic log method with configurable level */ + log(level: string, message?: string, data?: unknown, error?: Error): void; + /** Log error messages with optional error object and additional data */ + error(message?: string, error?: Error, data?: unknown): void; + /** Log warning messages with optional additional data */ + warn(message?: string, data?: unknown): void; + /** Log informational messages with optional additional data */ + info(message?: string, data?: unknown): void; + /** Log debug messages with optional additional data */ + debug(message?: string, data?: unknown): void; +} + +/** + * Configuration options for logger behavior + */ +export interface LoggerConfig { + /** + * Custom logger implementation to use instead of the default console logger + */ + customLogger?: Logger; + + /** + * Whether to enable mode-aware logging (suppress logs during replay) + * @defaultValue true + */ + modeAware?: boolean; +} + +/** + * Base interface for operation contexts. + * Provides logger access within step functions, wait conditions, and callbacks. + */ +export interface OperationContext { + logger: Logger; +} + +export type StepContext = OperationContext; +export type WaitForConditionContext = OperationContext; +export type WaitForCallbackContext = OperationContext; +``` + ### Configuration Types ```typescript diff --git a/packages/aws-durable-execution-sdk-js/src/types/durable-context.ts b/packages/aws-durable-execution-sdk-js/src/types/durable-context.ts index 6d15a4cf..42049557 100644 --- a/packages/aws-durable-execution-sdk-js/src/types/durable-context.ts +++ b/packages/aws-durable-execution-sdk-js/src/types/durable-context.ts @@ -1,5 +1,5 @@ import { Context } from "aws-lambda"; -import { Logger } from "./logger"; +import { Logger, LoggerConfig } from "./logger"; import { StepFunc, StepConfig } from "./step"; import { ChildFunc, ChildConfig } from "./child-context"; import { InvokeConfig } from "./invoke"; @@ -802,10 +802,12 @@ export interface DurableContext { ): Promise>; /** - * Sets a custom logger for this context - * @param logger - Custom logger implementation + * Configures logger behavior for this context + * + * @param config - Logger configuration options * @example * ```typescript + * // Set custom logger * const customLogger = { * log: (level, message, data, error) => console.log(`[${level}] ${message}`, data), * error: (message, error, data) => console.error(message, error, data), @@ -813,8 +815,17 @@ export interface DurableContext { * info: (message, data) => console.info(message, data), * debug: (message, data) => console.debug(message, data) * }; - * context.setCustomLogger(customLogger); + * context.configureLogger({ customLogger }); + * + * // Disable mode-aware logging to see logs during replay + * context.configureLogger({ modeAware: false }); + * + * // Both together + * context.configureLogger({ + * customLogger, + * modeAware: false + * }); * ``` */ - setCustomLogger(logger: Logger): void; + configureLogger(config: LoggerConfig): void; } diff --git a/packages/aws-durable-execution-sdk-js/src/types/logger.ts b/packages/aws-durable-execution-sdk-js/src/types/logger.ts index 08aaffd9..f4531d7b 100644 --- a/packages/aws-durable-execution-sdk-js/src/types/logger.ts +++ b/packages/aws-durable-execution-sdk-js/src/types/logger.ts @@ -15,6 +15,25 @@ export interface Logger { debug(message?: string, data?: unknown): void; } +/** + * Configuration options for logger behavior + * + * This interface supports partial configuration - you can provide only the properties + * you want to update. Omitted properties will retain their current values. + */ +export interface LoggerConfig { + /** + * Custom logger implementation to use instead of the default console logger + */ + customLogger?: Logger; + + /** + * Whether to enable mode-aware logging (suppress logs during replay) + * @defaultValue true + */ + modeAware?: boolean; +} + /** * Base interface for operation contexts. * Do not use directly - use specific context types like StepContext, WaitForConditionContext, etc. diff --git a/packages/aws-durable-execution-sdk-js/src/utils/logger/mode-aware-logger.test.ts b/packages/aws-durable-execution-sdk-js/src/utils/logger/mode-aware-logger.test.ts index 76c7d18d..0ddea48b 100644 --- a/packages/aws-durable-execution-sdk-js/src/utils/logger/mode-aware-logger.test.ts +++ b/packages/aws-durable-execution-sdk-js/src/utils/logger/mode-aware-logger.test.ts @@ -36,6 +36,7 @@ describe("Mode-Aware Logger", () => { const logger = createModeAwareLogger( DurableExecutionMode.ExecutionMode, mockCreateContextLogger, + false, ); logger.info("test message"); @@ -46,10 +47,11 @@ describe("Mode-Aware Logger", () => { ); }); - test("should not log in ReplayMode", () => { + test("should not log in ReplayMode when modeAware is enabled", () => { const logger = createModeAwareLogger( DurableExecutionMode.ReplayMode, mockCreateContextLogger, + true, ); logger.info("test message"); @@ -57,10 +59,11 @@ describe("Mode-Aware Logger", () => { expect(mockEnrichedLogger.info).not.toHaveBeenCalled(); }); - test("should not log in ReplaySucceededContext", () => { + test("should not log in ReplaySucceededContext when modeAware is enabled", () => { const logger = createModeAwareLogger( DurableExecutionMode.ReplaySucceededContext, mockCreateContextLogger, + true, ); logger.info("test message"); @@ -73,6 +76,7 @@ describe("Mode-Aware Logger", () => { createModeAwareLogger( DurableExecutionMode.ExecutionMode, mockCreateContextLogger, + false, stepPrefix, ); @@ -83,6 +87,7 @@ describe("Mode-Aware Logger", () => { createModeAwareLogger( DurableExecutionMode.ExecutionMode, mockCreateContextLogger, + false, ); expect(mockCreateContextLogger).toHaveBeenCalledWith("", undefined); @@ -92,6 +97,7 @@ describe("Mode-Aware Logger", () => { const logger = createModeAwareLogger( DurableExecutionMode.ExecutionMode, mockCreateContextLogger, + false, ); logger.log("custom", "log message", { data: "test" }, new Error("test")); @@ -118,10 +124,11 @@ describe("Mode-Aware Logger", () => { }); }); - test("should not call any logger methods in ReplayMode", () => { + test("should not call any logger methods in ReplayMode when modeAware is enabled", () => { const logger = createModeAwareLogger( DurableExecutionMode.ReplayMode, mockCreateContextLogger, + true, ); logger.log("custom", "log message"); diff --git a/packages/aws-durable-execution-sdk-js/src/utils/logger/mode-aware-logger.ts b/packages/aws-durable-execution-sdk-js/src/utils/logger/mode-aware-logger.ts index e3c9574f..c61c8a9a 100644 --- a/packages/aws-durable-execution-sdk-js/src/utils/logger/mode-aware-logger.ts +++ b/packages/aws-durable-execution-sdk-js/src/utils/logger/mode-aware-logger.ts @@ -3,13 +3,15 @@ import { Logger, DurableExecutionMode } from "../../types"; export const createModeAwareLogger = ( durableExecutionMode: DurableExecutionMode, createContextLogger: (stepId: string, attempt?: number) => Logger, + modeAwareEnabled: boolean, stepPrefix?: string, ): Logger => { // Use context logger factory with stepPrefix as step ID (or undefined for top level) const enrichedLogger = createContextLogger(stepPrefix || "", undefined); - // Only log if in ExecutionMode + // Only log if in ExecutionMode (when mode-aware is enabled) or always log (when disabled) const shouldLog = (): boolean => + !modeAwareEnabled || durableExecutionMode === DurableExecutionMode.ExecutionMode; return { From cc83c44af972386b6e9515c7cd9e131faf780b10 Mon Sep 17 00:00:00 2001 From: Pooya Paridel Date: Wed, 12 Nov 2025 15:09:38 -0800 Subject: [PATCH 2/2] refactor: use finally blocks instead of async/await for mode updates - Replace async/await pattern with finally blocks in invoke, wait, createCallback, waitForCallback - Add optional chaining for promise.finally() to handle undefined promises - Add README tip about modeAware: false for local development/debugging --- .../aws-durable-execution-sdk-js/README.md | 6 ++++ .../durable-context/durable-context.ts | 31 ++++++++++--------- 2 files changed, 22 insertions(+), 15 deletions(-) diff --git a/packages/aws-durable-execution-sdk-js/README.md b/packages/aws-durable-execution-sdk-js/README.md index e94c2e06..ae9d7ac4 100644 --- a/packages/aws-durable-execution-sdk-js/README.md +++ b/packages/aws-durable-execution-sdk-js/README.md @@ -422,6 +422,12 @@ context.configureLogger({ customLogger: myLogger }); The new `configureLogger()` method provides additional control over logging behavior with the `modeAware` option. +**Tip for local development:** Set `modeAware: false` to see all logs during replay, which can be helpful for debugging: + +```typescript +context.configureLogger({ modeAware: false }); +``` + ## Testing Locally Run durable functions locally for development: diff --git a/packages/aws-durable-execution-sdk-js/src/context/durable-context/durable-context.ts b/packages/aws-durable-execution-sdk-js/src/context/durable-context/durable-context.ts index 3a5c31c2..1bfc7fbb 100644 --- a/packages/aws-durable-execution-sdk-js/src/context/durable-context/durable-context.ts +++ b/packages/aws-durable-execution-sdk-js/src/context/durable-context/durable-context.ts @@ -238,7 +238,7 @@ class DurableContextImpl implements DurableContext { "invoke", this.executionContext.terminationManager, ); - return this.withModeManagement(async () => { + return this.withModeManagement(() => { const invokeHandler = createInvokeHandler( this.executionContext, this.checkpoint, @@ -257,9 +257,9 @@ class DurableContextImpl implements DurableContext { ); // Prevent unhandled promise rejections promise?.catch(() => {}); - const result = await promise; - this.checkAndUpdateReplayMode(); - return result; + return promise?.finally(() => { + this.checkAndUpdateReplayMode(); + }); }); } @@ -299,7 +299,7 @@ class DurableContextImpl implements DurableContext { "wait", this.executionContext.terminationManager, ); - return this.withModeManagement(async () => { + return this.withModeManagement(() => { const waitHandler = createWaitHandler( this.executionContext, this.checkpoint, @@ -314,8 +314,9 @@ class DurableContextImpl implements DurableContext { : waitHandler(nameOrDuration); // Prevent unhandled promise rejections promise?.catch(() => {}); - await promise; - this.checkAndUpdateReplayMode(); + return promise?.finally(() => { + this.checkAndUpdateReplayMode(); + }); }); } @@ -352,7 +353,7 @@ class DurableContextImpl implements DurableContext { "createCallback", this.executionContext.terminationManager, ); - return this.withModeManagement(async () => { + return this.withModeManagement(() => { const callbackFactory = createCallbackFactory( this.executionContext, this.checkpoint, @@ -364,9 +365,9 @@ class DurableContextImpl implements DurableContext { const promise = callbackFactory(nameOrConfig, maybeConfig); // Prevent unhandled promise rejections promise?.catch(() => {}); - const result = await promise; - this.checkAndUpdateReplayMode(); - return result; + return promise?.finally(() => { + this.checkAndUpdateReplayMode(); + }); }); } @@ -380,7 +381,7 @@ class DurableContextImpl implements DurableContext { "waitForCallback", this.executionContext.terminationManager, ); - return this.withModeManagement(async () => { + return this.withModeManagement(() => { const waitForCallbackHandler = createWaitForCallbackHandler( this.executionContext, this.runInChildContext.bind(this), @@ -392,9 +393,9 @@ class DurableContextImpl implements DurableContext { ); // Prevent unhandled promise rejections promise?.catch(() => {}); - const result = await promise; - this.checkAndUpdateReplayMode(); - return result; + return promise?.finally(() => { + this.checkAndUpdateReplayMode(); + }); }); }