From ac83a0f3242489762f3df0ef02001fed64110b69 Mon Sep 17 00:00:00 2001 From: Martin Jesper Low Madsen Date: Thu, 5 Jul 2018 16:10:43 +0200 Subject: [PATCH 01/13] Fixes usage with serverless-webpack and serverless-offline hooks --- index.js | 23 +++++++++++++++-------- proxy.js | 17 +++++++++-------- 2 files changed, 24 insertions(+), 16 deletions(-) diff --git a/index.js b/index.js index d83ed22..d3c7fdc 100644 --- a/index.js +++ b/index.js @@ -1,7 +1,7 @@ 'use strict'; const packagePath = 'node_modules/serverless-offline-direct-lambda'; -const handlerPath = `proxy.js`; +const handlerPath = 'proxy.js'; class ServerlessPlugin { constructor(serverless, options) { @@ -9,31 +9,37 @@ class ServerlessPlugin { this.options = options; this.hooks = { - "before:offline:start:init": this.startHandler.bind(this), + 'before:offline:start': this.startHandler.bind(this), }; } startHandler() { + let location = ''; + try { + location = this.serverless.service.custom['serverless-offline'].location; + this.serverless.service.custom['serverless-offline'].location = ''; + } catch (_) { } + this.serverless.cli.log('Running Serverless Offline with direct lambda support'); - addProxies(this.serverless.service.functions); + addProxies(this.serverless.service.functions, location); } } -const addProxies = functionsObject => { +const addProxies = (functionsObject, location) => { Object.keys(functionsObject).forEach(fn => { // filter out functions with event config, // leaving just those intended for direct lambda-to-lambda invocation const functionObject = functionsObject[fn]; - if (!functionObject.events || functionObject.events.length == 0) { - const pf = functionProxy(functionObject); + if (!functionObject.events || !functionObject.events.some((event) => event === 'http')) { + const pf = functionProxy(functionObject, location); functionsObject[pf.name] = pf; } }); }; -const functionProxy = functionBeingProxied => ({ +const functionProxy = (functionBeingProxied, location) => ({ name: `${functionBeingProxied.name}_proxy`, handler: `${packagePath}/proxy.handler`, events: [ @@ -46,8 +52,9 @@ const functionProxy = functionBeingProxied => ({ template: { 'application/json': JSON.stringify( { + location, + body: "$input.json('$')", targetHandler : functionBeingProxied.handler, - body: "$input.json('$')" } ) } diff --git a/proxy.js b/proxy.js index 680ae31..6bb0fec 100644 --- a/proxy.js +++ b/proxy.js @@ -1,24 +1,25 @@ const serializeError = require('serialize-error'); +const path = require('path'); -function handler(event, context, callback) { +async function handler(event, context, callback) { // extract the path to the handler (relative to the project root) // and the function to call on the handler - const [targetHandlerFile, targetHandlerFunction] = event.targetHandler.split("."); - const target = require('../../' + targetHandlerFile); + const [targetHandlerFile, targetHandlerFunction] = event.targetHandler.split('.'); + const target = require(path.resolve(__dirname, '../..', event.location, targetHandlerFile)); // call the target function - target[targetHandlerFunction](event.body, context, (error, response) => { + return target[targetHandlerFunction](event.body, context, (error, response) => { if (error) { callback(null, { StatusCode: 500, FunctionError: 'Handled', - Payload: serializeError(error) - }) + Payload: serializeError(error), + }); } else { callback(null, { StatusCode: 200, - Payload: JSON.stringify(response) - }) + Payload: JSON.stringify(response), + }); } }); } From 38f5c952e1133a54dbbdd1277d6ca2b6c55d84c2 Mon Sep 17 00:00:00 2001 From: Martin Jesper Low Madsen Date: Fri, 6 Jul 2018 14:12:26 +0200 Subject: [PATCH 02/13] Further mimics a real Lambda invocation where body contains the invocation parameters --- proxy.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/proxy.js b/proxy.js index 6bb0fec..5bb7258 100644 --- a/proxy.js +++ b/proxy.js @@ -2,13 +2,21 @@ const serializeError = require('serialize-error'); const path = require('path'); async function handler(event, context, callback) { + const { ClientContext, FunctionName, InvocationType, LogType, Payload } = event.body; + // extract the path to the handler (relative to the project root) // and the function to call on the handler const [targetHandlerFile, targetHandlerFunction] = event.targetHandler.split('.'); const target = require(path.resolve(__dirname, '../..', event.location, targetHandlerFile)); + const targetEvent = JSON.parse(Payload); + const targetContext = { + ...context, + clientContext: JSON.parse(Buffer.from(ClientContext, 'base64')), + }; + // call the target function - return target[targetHandlerFunction](event.body, context, (error, response) => { + return target[targetHandlerFunction](targetEvent, targetContext, (error, response) => { if (error) { callback(null, { StatusCode: 500, From b256cc01f059c2539cffb5f302e55de34a338e4a Mon Sep 17 00:00:00 2001 From: Martin Jesper Low Madsen Date: Fri, 6 Jul 2018 15:56:52 +0200 Subject: [PATCH 03/13] Forwards the proxied function's environment --- index.js | 1 + 1 file changed, 1 insertion(+) diff --git a/index.js b/index.js index d3c7fdc..1a2933e 100644 --- a/index.js +++ b/index.js @@ -42,6 +42,7 @@ const addProxies = (functionsObject, location) => { const functionProxy = (functionBeingProxied, location) => ({ name: `${functionBeingProxied.name}_proxy`, handler: `${packagePath}/proxy.handler`, + environment: functionBeingProxied.environment, events: [ { http: { From e62f8972c1a35c48cfdc64e70bfbfcdc73a421e0 Mon Sep 17 00:00:00 2001 From: Martin Jesper Low Madsen Date: Thu, 12 Jul 2018 12:58:09 +0200 Subject: [PATCH 04/13] Moves away from callback support internally in favor of promises --- proxy.js | 34 ++++++++++++++++++---------------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/proxy.js b/proxy.js index 5bb7258..0c7d204 100644 --- a/proxy.js +++ b/proxy.js @@ -1,7 +1,7 @@ const serializeError = require('serialize-error'); const path = require('path'); -async function handler(event, context, callback) { +export async function handler(event, context) { const { ClientContext, FunctionName, InvocationType, LogType, Payload } = event.body; // extract the path to the handler (relative to the project root) @@ -15,21 +15,23 @@ async function handler(event, context, callback) { clientContext: JSON.parse(Buffer.from(ClientContext, 'base64')), }; - // call the target function - return target[targetHandlerFunction](targetEvent, targetContext, (error, response) => { - if (error) { - callback(null, { - StatusCode: 500, - FunctionError: 'Handled', - Payload: serializeError(error), - }); - } else { - callback(null, { - StatusCode: 200, - Payload: JSON.stringify(response), - }); + const funcResult = new Promise((resolve, reject) => { + const result = target[targetHandlerFunction](targetEvent, targetContext, (error, response) => { + if (error) { + reject(error); + } else { + resolve(response); + } + }); + + if (result && typeof result.then === 'function' && typeof result.catch === 'function') { + result.then(resolve).catch(reject); } }); -} -module.exports.handler = handler; + try { + return { StatusCode: 200, Payload: JSON.stringify(await funcResult) }; + } catch (error) { + return { StatusCode: 500, FunctionError: 'Handled', Payload: serializeError(error) }; + } +} From 8198790d7cf854d86dea3b7a002d4a182b04375e Mon Sep 17 00:00:00 2001 From: Martin Jesper Low Madsen Date: Thu, 12 Jul 2018 13:05:31 +0200 Subject: [PATCH 05/13] Reverts back to module.exports temporarily to comply with Node.js 8.10 --- proxy.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/proxy.js b/proxy.js index 0c7d204..7b03ef6 100644 --- a/proxy.js +++ b/proxy.js @@ -1,7 +1,7 @@ const serializeError = require('serialize-error'); const path = require('path'); -export async function handler(event, context) { +async function handler(event, context) { const { ClientContext, FunctionName, InvocationType, LogType, Payload } = event.body; // extract the path to the handler (relative to the project root) @@ -35,3 +35,5 @@ export async function handler(event, context) { return { StatusCode: 500, FunctionError: 'Handled', Payload: serializeError(error) }; } } + +module.exports.handler = handler; From 55d02b031941c95641ef1d80e4da6e8695b8231d Mon Sep 17 00:00:00 2001 From: Martin Jesper Low Madsen Date: Wed, 15 Aug 2018 13:44:41 +0200 Subject: [PATCH 06/13] Assures ClientContext is set before writing clientContext to the proxied event --- proxy.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/proxy.js b/proxy.js index 7b03ef6..e0f6dfb 100644 --- a/proxy.js +++ b/proxy.js @@ -12,9 +12,12 @@ async function handler(event, context) { const targetEvent = JSON.parse(Payload); const targetContext = { ...context, - clientContext: JSON.parse(Buffer.from(ClientContext, 'base64')), }; + if (ClientContext) { + targetContext.clientContext = JSON.parse(Buffer.from(ClientContext, 'base64')); + } + const funcResult = new Promise((resolve, reject) => { const result = target[targetHandlerFunction](targetEvent, targetContext, (error, response) => { if (error) { From b083f17dff92a87159f109e84665e8e6ed19af2f Mon Sep 17 00:00:00 2001 From: Martin Jesper Low Madsen Date: Wed, 15 Aug 2018 14:19:31 +0200 Subject: [PATCH 07/13] Listens for `before:offline:start` and `before:offline:start:init` to trigger (once) for both `sls offline` and `sls offline start` --- index.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/index.js b/index.js index 1a2933e..e07e63e 100644 --- a/index.js +++ b/index.js @@ -8,8 +8,11 @@ class ServerlessPlugin { this.serverless = serverless; this.options = options; + const boundStartHandler = this.startHandler.bind(this); + this.hooks = { - 'before:offline:start': this.startHandler.bind(this), + 'before:offline:start': boundStartHandler, + 'before:offline:start:init': boundStartHandler, }; } From 6de8921de6927c7ec800f74335e6b7b04555d793 Mon Sep 17 00:00:00 2001 From: Martin Jesper Low Madsen Date: Wed, 15 Aug 2018 14:20:57 +0200 Subject: [PATCH 08/13] Fixes http filter logic for proxied functions --- index.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/index.js b/index.js index e07e63e..8327e0d 100644 --- a/index.js +++ b/index.js @@ -35,7 +35,8 @@ const addProxies = (functionsObject, location) => { // filter out functions with event config, // leaving just those intended for direct lambda-to-lambda invocation const functionObject = functionsObject[fn]; - if (!functionObject.events || !functionObject.events.some((event) => event === 'http')) { + if (!functionObject.events || + !functionObject.events.some((event) => Object.keys(event)[0] === 'http')) { const pf = functionProxy(functionObject, location); functionsObject[pf.name] = pf; } From cfc80b3a69a33095014fb7b0321ac58ec9f5f2a9 Mon Sep 17 00:00:00 2001 From: Martin Jesper Low Madsen Date: Sat, 13 Oct 2018 16:08:31 +0200 Subject: [PATCH 09/13] Update location to support `yarn link` --- index.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/index.js b/index.js index 8327e0d..391f9f9 100644 --- a/index.js +++ b/index.js @@ -17,12 +17,16 @@ class ServerlessPlugin { } startHandler() { + // Serverless Webpack overrides the location to its output directory. Set + // location to that directory. let location = ''; try { location = this.serverless.service.custom['serverless-offline'].location; this.serverless.service.custom['serverless-offline'].location = ''; } catch (_) { } + location = `${this.serverless.config.servicePath}/${location}`; + this.serverless.cli.log('Running Serverless Offline with direct lambda support'); addProxies(this.serverless.service.functions, location); From e95bfad8ab81e06eaf400db5911b4f44c29a4a7c Mon Sep 17 00:00:00 2001 From: Martin Jesper Low Madsen Date: Fri, 12 Oct 2018 17:04:00 +0200 Subject: [PATCH 10/13] Add support for AWS XRay tracing --- index.js | 22 +++++++++++++++++----- package.json | 3 +++ proxy.js | 26 ++++++++++++++++++++++---- 3 files changed, 42 insertions(+), 9 deletions(-) diff --git a/index.js b/index.js index 391f9f9..a12906d 100644 --- a/index.js +++ b/index.js @@ -29,11 +29,13 @@ class ServerlessPlugin { this.serverless.cli.log('Running Serverless Offline with direct lambda support'); - addProxies(this.serverless.service.functions, location); + addProxies(this.serverless.service.functions, + location, + this.serverless.service.provider.tracing === 'true'); } } -const addProxies = (functionsObject, location) => { +const addProxies = (functionsObject, location, tracing) => { Object.keys(functionsObject).forEach(fn => { // filter out functions with event config, @@ -41,13 +43,13 @@ const addProxies = (functionsObject, location) => { const functionObject = functionsObject[fn]; if (!functionObject.events || !functionObject.events.some((event) => Object.keys(event)[0] === 'http')) { - const pf = functionProxy(functionObject, location); + const pf = functionProxy(functionObject, location, tracing); functionsObject[pf.name] = pf; } }); }; -const functionProxy = (functionBeingProxied, location) => ({ +const functionProxy = (functionBeingProxied, location, tracing) => ({ name: `${functionBeingProxied.name}_proxy`, handler: `${packagePath}/proxy.handler`, environment: functionBeingProxied.environment, @@ -62,8 +64,18 @@ const functionProxy = (functionBeingProxied, location) => ({ 'application/json': JSON.stringify( { location, + headers: `{ + #set( $map = $input.params().header ) + #foreach($key in $map.keySet()) + "$util.escapeJavaScript($key)": "$util.escapeJavaScript($map.get($key))" + #if( $foreach.hasNext ) + , + #end + #end + }`, body: "$input.json('$')", - targetHandler : functionBeingProxied.handler, + targetHandler: functionBeingProxied.handler, + tracing, } ) } diff --git a/package.json b/package.json index 318ceb7..1261f0f 100644 --- a/package.json +++ b/package.json @@ -17,5 +17,8 @@ "license": "ISC", "dependencies": { "serialize-error": "^2.1.0" + }, + "devDependencies": { + "aws-xray-sdk": "^2.0.0" } } diff --git a/proxy.js b/proxy.js index e0f6dfb..6bd997f 100644 --- a/proxy.js +++ b/proxy.js @@ -1,5 +1,6 @@ -const serializeError = require('serialize-error'); +const awsXRay = require('aws-xray-sdk'); const path = require('path'); +const serializeError = require('serialize-error'); async function handler(event, context) { const { ClientContext, FunctionName, InvocationType, LogType, Payload } = event.body; @@ -7,7 +8,7 @@ async function handler(event, context) { // extract the path to the handler (relative to the project root) // and the function to call on the handler const [targetHandlerFile, targetHandlerFunction] = event.targetHandler.split('.'); - const target = require(path.resolve(__dirname, '../..', event.location, targetHandlerFile)); + const target = require(path.resolve(event.location, targetHandlerFile)); const targetEvent = JSON.parse(Payload); const targetContext = { @@ -19,7 +20,24 @@ async function handler(event, context) { } const funcResult = new Promise((resolve, reject) => { - const result = target[targetHandlerFunction](targetEvent, targetContext, (error, response) => { + let targetHandler = target[targetHandlerFunction]; + + if (event.tracing) { + const ns = awsXRay.getNamespace(); + if (!ns.active) { + const [Root, Parent, Sampled] = event.headers['X-Amzn-Trace-Id'].split(';'); + const segment = new awsXRay.Segment(targetHandlerFunction, Root.split('=')[1], Parent.split('=')[1]); + targetHandler = ns.bind((event, context, callback) => { + awsXRay.setSegment(segment); + target[targetHandlerFunction](event, context, (error, result) => { + segment.close(); + callback(error, result); + }); + }); + } + } + + const result = targetHandler(targetEvent, targetContext, (error, response) => { if (error) { reject(error); } else { @@ -35,7 +53,7 @@ async function handler(event, context) { try { return { StatusCode: 200, Payload: JSON.stringify(await funcResult) }; } catch (error) { - return { StatusCode: 500, FunctionError: 'Handled', Payload: serializeError(error) }; + return { StatusCode: 500, FunctionError: 'Handled (serverless-offline-direct-lambda)', Payload: serializeError(error) }; } } From 5a11a2246dd05ef774c152064e490eb62fd91abc Mon Sep 17 00:00:00 2001 From: Martin Jesper Low Madsen Date: Sat, 13 Oct 2018 12:10:02 +0200 Subject: [PATCH 11/13] Use handler name for XRay segment --- index.js | 1 + proxy.js | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/index.js b/index.js index a12906d..8d9a834 100644 --- a/index.js +++ b/index.js @@ -75,6 +75,7 @@ const functionProxy = (functionBeingProxied, location, tracing) => ({ }`, body: "$input.json('$')", targetHandler: functionBeingProxied.handler, + handlerName: functionBeingProxied.name, tracing, } ) diff --git a/proxy.js b/proxy.js index 6bd997f..d21e986 100644 --- a/proxy.js +++ b/proxy.js @@ -26,7 +26,7 @@ async function handler(event, context) { const ns = awsXRay.getNamespace(); if (!ns.active) { const [Root, Parent, Sampled] = event.headers['X-Amzn-Trace-Id'].split(';'); - const segment = new awsXRay.Segment(targetHandlerFunction, Root.split('=')[1], Parent.split('=')[1]); + const segment = new awsXRay.Segment(event.handlerName, Root.split('=')[1], Parent.split('=')[1]); targetHandler = ns.bind((event, context, callback) => { awsXRay.setSegment(segment); target[targetHandlerFunction](event, context, (error, result) => { From 768bc5af81ff1f78727a436c63481990d725d653 Mon Sep 17 00:00:00 2001 From: Martin Jesper Low Madsen Date: Mon, 15 Oct 2018 13:38:09 +0200 Subject: [PATCH 12/13] Don't rely on XRay tracing header --- proxy.js | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/proxy.js b/proxy.js index d21e986..a7b8ca8 100644 --- a/proxy.js +++ b/proxy.js @@ -25,15 +25,18 @@ async function handler(event, context) { if (event.tracing) { const ns = awsXRay.getNamespace(); if (!ns.active) { - const [Root, Parent, Sampled] = event.headers['X-Amzn-Trace-Id'].split(';'); - const segment = new awsXRay.Segment(event.handlerName, Root.split('=')[1], Parent.split('=')[1]); - targetHandler = ns.bind((event, context, callback) => { - awsXRay.setSegment(segment); - target[targetHandlerFunction](event, context, (error, result) => { - segment.close(); - callback(error, result); + const amazonTracing = event.headers['X-Amzn-Trace-Id']; + if (amazonTracing) { + const [Root, Parent, Sampled] = amazonTracing.split(';'); + const segment = new awsXRay.Segment(event.handlerName, Root.split('=')[1], Parent.split('=')[1]); + targetHandler = ns.bind((event, context, callback) => { + awsXRay.setSegment(segment); + target[targetHandlerFunction](event, context, (error, result) => { + segment.close(); + callback(error, result); + }); }); - }); + } } } From bb93cac948e3622f0d07348c79359de7900c967c Mon Sep 17 00:00:00 2001 From: Martin Madsen Date: Tue, 20 Apr 2021 08:14:07 +0200 Subject: [PATCH 13/13] Support handler paths with periods and fallback to an empty location if not configured --- index.js | 2 +- proxy.js | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/index.js b/index.js index 8d9a834..803881d 100644 --- a/index.js +++ b/index.js @@ -21,7 +21,7 @@ class ServerlessPlugin { // location to that directory. let location = ''; try { - location = this.serverless.service.custom['serverless-offline'].location; + location = this.serverless.service.custom['serverless-offline'].location || location; this.serverless.service.custom['serverless-offline'].location = ''; } catch (_) { } diff --git a/proxy.js b/proxy.js index a7b8ca8..1c5c2bc 100644 --- a/proxy.js +++ b/proxy.js @@ -7,8 +7,9 @@ async function handler(event, context) { // extract the path to the handler (relative to the project root) // and the function to call on the handler - const [targetHandlerFile, targetHandlerFunction] = event.targetHandler.split('.'); - const target = require(path.resolve(event.location, targetHandlerFile)); + const targetParts = event.targetHandler.split('.'); + const [targetHandlerFunction, ...targetHandlerFile] = targetParts.reverse(); + const target = require(path.resolve(event.location, targetHandlerFile.reverse().join('.'))); const targetEvent = JSON.parse(Payload); const targetContext = {