serverless-spy
Version:
CDK-based library for writing elegant integration tests on AWS serverless architecture and an additional web console to monitor events in real time.
254 lines (214 loc) • 6.77 kB
text/typescript
import { Callback, Context, Handler } from 'aws-lambda';
import { serializeError } from 'serialize-error';
// @ts-ignore
import { load } from './aws/UserFunction';
import { FunctionConsoleSpyEvent } from '../common/spyEvents/FunctionConsoleSpyEvent';
import { FunctionContext } from '../common/spyEvents/FunctionContext';
import { FunctionErrorSpyEvent } from '../common/spyEvents/FunctionErrorSpyEvent';
import { FunctionRequestSpyEvent } from '../common/spyEvents/FunctionRequestSpyEvent';
import { FunctionResponseSpyEvent } from '../common/spyEvents/FunctionResponseSpyEvent';
import { SpyEventSender } from '../common/SpyEventSender';
import { envVariableNames } from '../src/common/envVariableNames';
// @ts-ignore
const ORIGINAL_HANDLER_KEY = 'ORIGINAL_HANDLER';
const subscribedToSQS =
process.env[envVariableNames.SSPY_SUBSCRIBED_TO_SQS] === 'true';
const debugMode = process.env[envVariableNames.SSPY_DEBUG] === 'true';
const oldConsoleLog = console.log;
const oldConsoleWarn = console.warn;
const oldConsoleDebug = console.debug;
const oldConsoleInfo = console.info;
const oldConsoleError = console.error;
let currentEvent: any;
let currentContext: FunctionContext | undefined;
let promises: Promise<any>[] = [];
interceptConsole();
const spyEventSender = new SpyEventSender({
log,
logError,
scope: process.env['SSPY_ROOT_STACK']!,
iotEndpoint: process.env['SSPY_IOT_ENDPOINT']!,
});
// Wrap original handler.
// Handler can be async or non-async:
// https://docs.aws.amazon.com/lambda/latest/dg/nodejs-prog-model-handler.html
export const handler = async (
event: any,
context: Context,
callback: Callback
): Promise<any | undefined> => {
await spyEventSender.connect();
const contextSpy: FunctionContext = {
functionName: context.functionName,
awsRequestId: context.awsRequestId,
identity: context.identity,
clientContext: context.clientContext,
};
currentEvent = event;
currentContext = contextSpy;
promises = [];
log('Request', JSON.stringify(event));
if (subscribedToSQS) {
// send raw message
log('Send raw message for SQS');
const p = sendRawSpyEvent(event);
promises.push(p);
}
const key = `Function#${
process.env[envVariableNames.SSPY_FUNCTION_NAME]
}#Request`;
const p = sendLambdaSpyEvent(key, <FunctionRequestSpyEvent>{
request: event,
context: contextSpy,
});
promises.push(p);
const originalHandler = await getOriginalHandler();
const fail = (error: any) => {
logError(error);
const errorSerialized = serializeError(error);
const key = `Function#${
process.env[envVariableNames.SSPY_FUNCTION_NAME]
}#Error`;
const p = sendLambdaSpyEvent(key, <FunctionErrorSpyEvent>{
request: event,
error: errorSerialized,
context: contextSpy,
});
promises.push(p);
currentEvent = undefined;
currentContext = undefined;
return Promise.all(promises);
};
const succeed = (response: any) => {
log('Response', JSON.stringify(response));
const key = `Function#${
process.env[envVariableNames[envVariableNames.SSPY_FUNCTION_NAME]]
}#Response`;
const p = sendLambdaSpyEvent(key, <FunctionResponseSpyEvent>{
request: event,
response,
context: contextSpy,
});
promises.push(p);
currentEvent = undefined;
currentContext = undefined;
return Promise.all(promises);
};
const newCallback = (err: any, data: any) => {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
(err ? fail(err) : succeed(data)).then(() => {
callback(err, data);
});
};
try {
const result = originalHandler(event, context, newCallback);
// Async handler returns Promise
if (isPromise(result)) {
return await new Promise((resolve, reject) => {
(result as Promise<any>)
.then((response: any) =>
// The response is received via Promise
succeed(response).then(() => {
resolve(response);
})
)
.catch((error: any) =>
fail(error).then(() => {
reject(error);
})
);
});
}
} catch (error) {
// Even if the original handler is not async, we return the promise as an async handler so we can send an error message
// eslint-disable-next-line @typescript-eslint/return-await
return new Promise((_, reject) =>
fail(error).then(() => {
reject(error);
})
);
} finally {
await spyEventSender.close();
}
};
function interceptConsole() {
const sendLogs = (
type: 'log' | 'debug' | 'info' | 'error' | 'warn',
args?: any[]
) => {
if (!currentContext) return;
log(`Console ${type}`, JSON.stringify(args));
const message = args?.shift();
const key = `Function#${
process.env[envVariableNames.SSPY_FUNCTION_NAME]
}#Console`;
const p = sendLambdaSpyEvent(key, <FunctionConsoleSpyEvent>{
request: currentEvent,
context: currentContext,
console: {
type,
message,
optionalParams: args,
},
});
promises.push(p);
};
console.log = function (...args: any[]) {
sendLogs('log', args);
oldConsoleLog.apply(console, args);
};
console.warn = function (...args: any[]) {
sendLogs('warn', args);
oldConsoleWarn.apply(console, args);
};
console.debug = function (...args: any[]) {
sendLogs('debug', args);
oldConsoleDebug.apply(console, args);
};
console.info = function (...args: any[]) {
sendLogs('info', args);
oldConsoleInfo.apply(console, args);
};
console.error = function (...args: any[]) {
sendLogs('error', args);
oldConsoleError.apply(console, args);
};
}
function isPromise(obj: any): boolean {
return typeof obj?.then === 'function';
}
async function sendLambdaSpyEvent(
serviceKey: string,
data: {
request: any;
response?: any;
error?: any;
}
) {
await sendRawSpyEvent({
data,
serviceKey,
});
}
async function sendRawSpyEvent(data: any) {
await spyEventSender.publishSpyEvent(data);
}
async function getOriginalHandler(): Promise<Handler> {
log('Original handler', process.env[ORIGINAL_HANDLER_KEY]);
if (process.env[ORIGINAL_HANDLER_KEY] === undefined)
throw Error('Missing original handler');
return load(
process.env.LAMBDA_TASK_ROOT!,
process.env[ORIGINAL_HANDLER_KEY]
) as Promise<Handler>;
}
function log(message: string, ...optionalParams: any[]) {
if (debugMode) {
oldConsoleDebug('SSPY EXTENSION', message, ...optionalParams);
}
}
function logError(message: string, ...optionalParams: any[]) {
if (debugMode) {
oldConsoleError('SSPY EXTENSION', message, ...optionalParams);
}
}