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..ae9d7ac4 100644 --- a/packages/aws-durable-execution-sdk-js/README.md +++ b/packages/aws-durable-execution-sdk-js/README.md @@ -395,16 +395,39 @@ 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. + +**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 478d7791..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 @@ -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, ); } @@ -254,7 +257,9 @@ class DurableContextImpl implements DurableContext { ); // Prevent unhandled promise rejections promise?.catch(() => {}); - return promise; + return promise?.finally(() => { + this.checkAndUpdateReplayMode(); + }); }); } @@ -309,12 +314,34 @@ class DurableContextImpl implements DurableContext { : waitHandler(nameOrDuration); // Prevent unhandled promise rejections promise?.catch(() => {}); - return promise; + return promise?.finally(() => { + 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( @@ -338,7 +365,9 @@ class DurableContextImpl implements DurableContext { const promise = callbackFactory(nameOrConfig, maybeConfig); // Prevent unhandled promise rejections promise?.catch(() => {}); - return promise; + return promise?.finally(() => { + this.checkAndUpdateReplayMode(); + }); }); } @@ -364,7 +393,9 @@ class DurableContextImpl implements DurableContext { ); // Prevent unhandled promise rejections promise?.catch(() => {}); - return promise; + return promise?.finally(() => { + this.checkAndUpdateReplayMode(); + }); }); } 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 {