Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -194,4 +194,4 @@
"size": 214426,
"gitCommit": "9801dc79f2a4a980f971e2de1b85aa23b4c7a579"
}
]
]
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,10 @@
memoryLimitInMB: "128",
logGroupName: "/aws/lambda/mock-function",
logStreamName: "2023/01/01/[$LATEST]abcdef123456",
getRemainingTimeInMillis: () => 30000,

Check warning on line 63 in packages/lambda-durable-functions-sdk-js/src/context/durable-context/durable-context.test.ts

View workflow job for this annotation

GitHub Actions / build (22.x)

Missing return type on function
done: () => {},

Check warning on line 64 in packages/lambda-durable-functions-sdk-js/src/context/durable-context/durable-context.test.ts

View workflow job for this annotation

GitHub Actions / build (22.x)

Missing return type on function
fail: () => {},

Check warning on line 65 in packages/lambda-durable-functions-sdk-js/src/context/durable-context/durable-context.test.ts

View workflow job for this annotation

GitHub Actions / build (22.x)

Missing return type on function
succeed: () => {},

Check warning on line 66 in packages/lambda-durable-functions-sdk-js/src/context/durable-context/durable-context.test.ts

View workflow job for this annotation

GitHub Actions / build (22.x)

Missing return type on function
};

// Setup mocks
Expand Down Expand Up @@ -136,7 +136,8 @@
mockExecutionContext,
mockCheckpointHandler,
mockParentContext,
expect.any(Function),
expect.any(Function), // createStepId
expect.any(Function), // createContextLogger
);
expect(mockStepHandler).toHaveBeenCalledWith("test-step", stepFn, options);
});
Expand Down Expand Up @@ -428,7 +429,7 @@
mockExecutionContext,
mockParentContext,
);
expect(typeof durableContext.configureLogger).toBe("function");
expect(typeof durableContext.setCustomLogger).toBe("function");
});

it("should configure custom logger through DurableContext", () => {
Expand All @@ -446,13 +447,49 @@
};

// Configure custom logger
durableContext.configureLogger(mockCustomLogger);
durableContext.setCustomLogger(mockCustomLogger);

// Verify that the custom logger was set by checking the setCustomLogger was called
// Since the step handler is mocked, we can't test the full integration here
// but we can verify the method exists and doesn't throw
expect(() =>
durableContext.configureLogger(mockCustomLogger),
durableContext.setCustomLogger(mockCustomLogger),
).not.toThrow();
});

it("should use default logger when no custom logger is set", () => {
const consoleSpy = jest.spyOn(console, "log").mockImplementation();

// Temporarily restore the real step handler to test actual logger usage
const originalCreateStepHandler = jest.requireActual(
"../../handlers/step-handler/step-handler",
).createStepHandler;
(createStepHandler as jest.Mock).mockImplementationOnce(
originalCreateStepHandler,
);

const durableContext = createDurableContext(
mockExecutionContext,
mockParentContext,
);

// This will use the real step handler which will call the context logger
const stepFn = jest.fn().mockImplementation(async (ctx) => {
// Use all logger methods to ensure full coverage of createDefaultLogger
ctx.logger.log("custom", "test message");
ctx.logger.info("info test");
ctx.logger.error("error test", new Error("test"));
ctx.logger.warn("warn test");
ctx.logger.debug("debug test");
return "result";
});

// This should trigger the default logger
durableContext.step("test-step", stepFn);

// Verify console.log was called
expect(consoleSpy).toHaveBeenCalled();

consoleSpy.mockRestore();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,29 @@
import { createParallelHandler } from "../../handlers/parallel-handler/parallel-handler";
import { createPromiseHandler } from "../../handlers/promise-handler/promise-handler";
import { createConcurrentExecutionHandler } from "../../handlers/concurrent-execution-handler/concurrent-execution-handler";
import { setCustomLogger } from "../../utils/logger/structured-logger";
import { createContextLoggerFactory } from "../../utils/logger/context-logger";
import { createDefaultLogger } from "../../utils/logger/default-logger";

export const createDurableContext = (
executionContext: ExecutionContext,
parentContext: Context,
stepPrefix?: string,
checkpointToken?: string,
): DurableContext => {
// Local logger state for this context instance
let contextLogger: Logger | null = null;

// Local getter function for this context
const getLogger = (): Logger => {
return contextLogger || createDefaultLogger();
};

// Create context logger factory
const createContextLogger = createContextLoggerFactory(
executionContext,
getLogger,
);

let stepCounter = 0;
const checkpoint = createCheckpoint(executionContext, checkpointToken || "");

Expand All @@ -55,6 +70,7 @@
checkpoint,
parentContext,
createStepId,
createContextLogger,
);
return stepHandler(nameOrFn, fnOrOptions, maybeOptions);
};
Expand Down Expand Up @@ -102,8 +118,8 @@
};

const map: DurableContext["map"] = <T>(
nameOrItems: string | undefined | any[],

Check warning on line 121 in packages/lambda-durable-functions-sdk-js/src/context/durable-context/durable-context.ts

View workflow job for this annotation

GitHub Actions / build (22.x)

Unexpected any. Specify a different type
itemsOrMapFunc?: any[] | MapFunc<T>,

Check warning on line 122 in packages/lambda-durable-functions-sdk-js/src/context/durable-context/durable-context.ts

View workflow job for this annotation

GitHub Actions / build (22.x)

Unexpected any. Specify a different type
mapFuncOrConfig?: MapFunc<T> | MapConfig,
maybeConfig?: MapConfig,
) => {
Expand Down Expand Up @@ -153,8 +169,8 @@

const promise = createPromiseHandler(step);

const configureLogger = (logger: Logger): void => {
setCustomLogger(logger);
const setCustomLogger = (logger: Logger): void => {
contextLogger = logger;
};

return {
Expand All @@ -168,13 +184,14 @@
executionContext,
checkpoint,
createStepId,
createContextLogger,
),
createCallback,
waitForCallback,
map,
parallel,
executeConcurrently,
promise,
configureLogger,
setCustomLogger,
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -67,25 +67,36 @@ describe("Step Handler", () => {
mockCheckpoint = createMockCheckpoint();
mockParentContext = { awsRequestId: "mock-request-id" };
createStepId = jest.fn().mockReturnValue("test-step-id");

const mockLogger = {
log: jest.fn(),
info: jest.fn(),
error: jest.fn(),
warn: jest.fn(),
debug: jest.fn(),
};
const createMockEnrichedLogger = () => mockLogger;

stepHandler = createStepHandler(
mockExecutionContext,
mockCheckpoint,
mockParentContext,
createStepId,
createMockEnrichedLogger,
);

// Reset the mock for retryPresets.default
(retryPresets.default as jest.Mock).mockReset();
});

test("should execute step function with Telemetry", async () => {
test("should execute step function with StepContext", async () => {
const stepFn = jest.fn().mockResolvedValue("step-result");

const result = await stepHandler("test-step", stepFn);

expect(result).toBe("step-result");
expect(stepFn).toHaveBeenCalledTimes(1);
// Verify that Telemetry was passed to the step function
// Verify that StepContext was passed to the step function
expect(stepFn.mock.calls[0].length).toBe(1);
expect(stepFn.mock.calls[0][0]).toHaveProperty("logger");
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import {
RetryDecision,
StepSemantics,
OperationSubType,
Telemetry,
StepContext,
Logger,
} from "../../types";
import { Context } from "aws-lambda";
import {
Expand All @@ -26,7 +27,6 @@ import {
import { isUnrecoverableError } from "../../errors/unrecoverable-error/unrecoverable-error";
import { OperationInterceptor } from "../../mocks/operation-interceptor";
import { createErrorObjectFromError } from "../../utils/error-object/error-object";
import { createStructuredLogger } from "../../utils/logger/structured-logger";

const waitForTimer = <T>(
context: ExecutionContext,
Expand All @@ -39,14 +39,15 @@ const waitForTimer = <T>(
reason: TerminationReason.RETRY_SCHEDULED,
message: `Retry scheduled for ${name || stepId}`,
});
return new Promise<T>(() => { });
return new Promise<T>(() => {});
};

export const createStepHandler = (
context: ExecutionContext,
checkpoint: ReturnType<typeof createCheckpoint>,
parentContext: Context,
createStepId: () => string,
createContextLogger: (stepId: string, attempt?: number) => Logger,
) => {
return async <T>(
nameOrFn: string | undefined | StepFunc<T>,
Expand Down Expand Up @@ -158,7 +159,15 @@ export const createStepHandler = (
// READY: Timer completed, execute step function
// STARTED: Retry after error (AtLeastOncePerRetry semantics), execute step function
// undefined: First execution, execute step function
return executeStep(context, checkpoint, stepId, name, fn, options);
return executeStep(
context,
checkpoint,
stepId,
name,
fn,
createContextLogger,
options,
);
};
};

Expand Down Expand Up @@ -195,6 +204,7 @@ export const executeStep = async <T>(
stepId: string,
name: string | undefined,
fn: StepFunc<T>,
createContextLogger: (stepId: string, attempt?: number) => Logger,
options?: StepConfig<T>,
): Promise<T> => {
// Determine step semantics (default to AT_LEAST_ONCE_PER_RETRY if not specified)
Expand Down Expand Up @@ -228,23 +238,19 @@ export const executeStep = async <T>(
}

try {
// Get current attempt number for logger
// Get current attempt number for logger enrichment
const stepData = context.getStepData(stepId);
const currentAttempt = stepData?.StepDetails?.Attempt || 0;

// Create telemetry with logger
const telemetry: Telemetry = {
logger: createStructuredLogger({
stepId,
executionId: context.durableExecutionArn,
attempt: currentAttempt,
}),
// Create step context with enriched logger
const stepContext: StepContext = {
logger: createContextLogger(stepId, currentAttempt),
};

// Execute the step function with stepUtil
// Execute the step function with stepContext
const result = await OperationInterceptor.forExecution(
context.durableExecutionArn,
).execute(name, () => fn(telemetry));
).execute(name, () => fn(stepContext));

// Serialize the result for consistency
const serializedResult = await safeSerialize(
Expand Down Expand Up @@ -309,7 +315,7 @@ export const executeStep = async <T>(
});

// Return a never-resolving promise to ensure the execution doesn't continue
return new Promise<T>(() => { });
return new Promise<T>(() => {});
}

const stepData = context.getStepData(stepId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,15 @@ describe("waitForCallback handler", () => {
let stepIdCounter: number;
let mockRunInChildContext: jest.Mock;

const createMockLogger = () => ({
log: jest.fn(),
info: jest.fn(),
error: jest.fn(),
warn: jest.fn(),
debug: jest.fn(),
});
const createMockEnrichedLogger = () => createMockLogger();

beforeEach(() => {
stepIdCounter = 0;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import {
CreateCallbackConfig,
DurableContext,
OperationSubType,
Telemetry,
WaitForCallbackContext,
StepContext,
Logger,
} from "../../types";
import { log } from "../../utils/logger/logger";

Expand Down Expand Up @@ -69,12 +71,17 @@ export const createWaitForCallbackHandler = (
});

// Execute the submitter step (submitter is now mandatory)
await childCtx.step(async (telemetry: Telemetry) => {
await childCtx.step(async (stepContext: StepContext) => {
// Use the step's built-in logger instead of creating a new one
const callbackContext: WaitForCallbackContext = {
logger: stepContext.logger,
};

log(context.isVerbose, "📤", "Executing submitter:", {
callbackId,
name,
});
await submitter(callbackId, telemetry);
await submitter(callbackId, callbackContext);
log(context.isVerbose, "✅", "Submitter completed:", {
callbackId,
name,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,10 +67,20 @@ describe("WaitForCondition Handler", () => {
mockExecutionRunner,
);

const mockLogger = {
log: jest.fn(),
info: jest.fn(),
error: jest.fn(),
warn: jest.fn(),
debug: jest.fn(),
};
const createMockEnrichedLogger = () => mockLogger;

waitForConditionHandler = createWaitForConditionHandler(
mockExecutionContext,
mockCheckpoint,
createStepId,
createMockEnrichedLogger,
);
});

Expand Down
Loading
Loading