sigfox-aws
Version:
Framework for building a Sigfox server, based on Amazon Web Services and Lambda Functions
877 lines (799 loc) • 33.9 kB
JavaScript
// region Introduction
// sigfox-aws is a framework for building a Sigfox server, based
// on Amazon Web Services and AWS IoT. This module contains the framework functions
// used by sigfox-aws Lambda Functions. They should also work with Linux, MacOS
// and Ubuntu on Windows for unit testing.
/* eslint-disable max-len,import/no-unresolved,import/newline-after-import,arrow-body-style,camelcase,no-nested-ternary,no-underscore-dangle */
// //////////////////////////////////////////////////////////////////////////////////// endregion
// region Declarations - Helper constants to detect if we are running on Google Cloud or AWS.
const isGoogleCloud = !!process.env.FUNCTION_NAME || !!process.env.GAE_SERVICE;
const isAWS = !!process.env.AWS_LAMBDA_FUNCTION_NAME;
const isProduction = (process.env.NODE_ENV === 'production'); // True on production server.
const functionName = process.env.AWS_LAMBDA_FUNCTION_NAME || 'unknown_function';
const logName = process.env.LOGNAME || 'sigfox-aws';
if (process.env.AWS_EXECUTION_ENV && process.env.AWS_EXECUTION_ENV.indexOf('AWS_Lambda') >= 0 && !isProduction) {
// Confirm that NODE_ENV is set to "production". This is enforced in Google Cloud but not AWS.
throw new Error('NODE_ENV must be set to "production" in AWS Lambda environment');
}
process.env.AWS_XRAY_DEBUG_MODE = 'TRUE';
process.env.PACKAGE_VERSION = require('./package.json').version;
console.log({ gcloud_aws_version: process.env.PACKAGE_VERSION });
// //////////////////////////////////////////////////////////////////////////////////// endregion
// region Utility Functions
// //////////////////////////////////////////////////////////////////////////////////// endregion
// region Instrumentation Functions: Trace the execution of this Sigfox Callback across multiple Cloud Functions via AWS X-Ray
// eslint-disable-next-line no-unused-vars
// Allow AWS X-Ray to capture trace.
// eslint-disable-next-line import/no-unresolved
const AWSXRay = require('aws-xray-sdk-core');
AWSXRay.setStreamingThreshold(0); // TODO: Send XRay events immediately.
AWSXRay.middleware.setSamplingRules({
default: {
fixed_target: 100, // default 1
rate: 0.90, // default 0.05
},
version: 1,
});
// Clear the AWS Xray whitelist to disallow any AWS functions to be traced.
/* AWSXRay.setAWSWhitelist({
services: {
},
}); */
// Clear the AWS Xray whitelist and allow only AWS Lambda to be traced.
/* AWSXRay.setAWSWhitelist({
services: {
lambda: {
operations: {
invoke: {
request_parameters: [
'FunctionName',
'InvocationType',
'LogType',
'Qualifier',
],
response_parameters: [
'FunctionError',
'StatusCode',
],
},
invokeAsync: {
request_parameters: [
'FunctionName',
],
response_parameters: [
'Status',
],
},
},
},
},
}); */
// Extend the AWS Xray whitelist to allow these functions to log.
/* AWSXRay.appendAWSWhitelist({
services: {
xray: {
operations: {
putTraceSegments: {},
},
},
iot: {
operations: {
describeEndpoint: {},
},
},
iotdata: {
operations: {
publish: {},
},
},
},
}); */
// Create the AWS SDK instance. aws-sdk is automatically provided in AWS Lambda, no need to add to dependencies.
// eslint-disable-next-line import/no-extraneous-dependencies
const AWS = require('aws-sdk'); // Disable Xray logging for AWS requests.
/* const AWS = isProduction
? AWSXRay.captureAWS(require('aws-sdk')) // Enable Xray logging for AWS requests.
: require('aws-sdk'); */
if (isProduction) AWS.config.update({ region: process.env.AWS_REGION });
else AWS.config.loadFromPath('./aws-credentials.json');
// TODO: Create spans and traces for logging performance.
const rootSpanStub = {
startSpan: (/* rootSpanName, labels */) => ({
end: () => ({}),
}),
end: () => ({}),
};
// Remember the current AWS XRay trace.
let parentSegmentId = null;
let childSegmentId = null;
let parentSegment = null;
let childSegment = null;
let traceId = null;
// Prefix all segment names by the version number.
// const namePrefix = ['a', process.env.PACKAGE_VERSION.split('.').join(''), '_'].join('');
const namePrefix = ''; // No prefix for segment name.
// Random prefix for segment ID.
let segmentPrefix = '';
function sendTrace(req, segment) {
// Send the AWS XRay segment to AWS. Returns a promise.
const params = {
TraceSegmentDocuments: [
JSON.stringify(segment),
],
};
const xray = new AWS.XRay();
return xray.putTraceSegments(params).promise()
.then((res) => { console.log('sendSegment', segment, res); return res; })
.catch(error => console.error('sendSegment', segment, error.message, error.stack));
}
function createTraceSegment(traceId0, segmentId, parentSegmentId0, name0, user, annotations, metadata, startTime, comment) {
// Create a new AWS XRay segment. startTime (optional) is number of milliseconds since Jan 1 1970.
// const suffix = ` (${process.env.PACKAGE_VERSION.split('.').join('')})`;
const suffix = '';
const name = (namePrefix && namePrefix.length > 0)
? name0.replace(namePrefix, '')
: name0;
let method = '';
let url = suffix;
if (comment) {
const commentSplit = comment.split(' ', 2);
method = commentSplit[0].toUpperCase();
url = comment.substr(method.length + 1) + suffix;
}
const seqNumber = (annotations && annotations.seqNumber !== undefined) ? annotations.seqNumber : 0;
const newSegment = {
name: (namePrefix || '') + name,
id: segmentId,
start_time: (startTime || Date.now()) / 1000.0,
trace_id: traceId0,
in_progress: true,
http: {
request: {
// Log the device ID and sequence number into the URL.
method,
url,
client_ip: `${seqNumber}.0.0.0`,
},
response: {
content_length: -1,
status: seqNumber,
},
},
};
if (parentSegmentId0) newSegment.parent_id = parentSegmentId0;
if (user) newSegment.user = user;
if (annotations) newSegment.annotations = annotations;
if (metadata) newSegment.metadata = metadata;
return newSegment;
}
function openTraceSegment(traceId0, segmentId, parentSegmentId0, name0, user, annotations, metadata, startTime, comment) {
// Create a new AWS XRay segment and send to AWS. startTime (optional) is number of milliseconds since Jan 1 1970.
const segment = createTraceSegment(traceId0, segmentId, parentSegmentId0, name0, user, annotations, metadata, startTime, comment);
sendTrace({}, segment);
return segment;
}
function closeTraceSegment(segment) {
// Close the AWS XRay segment by sending the segment with end time to AWS.
// Returns a promise.
// eslint-disable-next-line no-param-reassign
segment.end_time = Date.now() / 1000.0; // eslint-disable-next-line no-param-reassign
if (segment.in_progress) delete segment.in_progress;
return sendTrace({}, segment)
.catch(error => console.error('closeSegment', error.message, error.stack));
}
/* function newTraceId() {
// Return a new Xray trace ID to identify a new request.
const trace_id_time = Math.floor(Date.now() / 1000).toString(16);
const trace_id = `1-${trace_id_time}-123456789012345678901234`; // 8 then 24 hex digits
return trace_id;
} */
function composeTraceAnnotations(payload) {
// Compose an AWS XRay Annotations object from the payload body.
// The annotations object will contain the Sigfox message values.
const annotations = {};
// Get the values from body, else from payload if already expanded.
const body = payload.body || payload || {};
for (const key of Object.keys(body)) {
// Log only scalar values.
const val = body[key];
if (val === null || val === undefined) continue;
if (typeof val !== 'string' && typeof val !== 'number' && typeof val !== 'boolean') continue;
annotations[key] = val;
}
return annotations;
}
function getTraceMetadata(payload) {
// Return the metadata from the payload that will be logged to AWS XRay. This should be everything in the
// message except the Sigfox message body, which is already in Annotations.
const metadata = Object.assign({}, payload.metadata || payload || {});
// Delete the body if it exists.
if (metadata.body) delete metadata.body;
return metadata;
}
let lastSegmentId = null;
function newTraceSegmentId() {
// Return a unique new XRay segment ID to identify the segment of running request code trace.
// Segment IDs must be 16 hex digits. We simply take the current epoch time
// and convert to hex.
const time = Date.now();
let segmentId = null;
for (let i = 0; i < 2; i += 1) {
// Loop twice if the segmentId is same as previous.
const timeHex = Math.floor(time + i).toString(16);
segmentId = (`0000000000000000${segmentPrefix}0${timeHex}`);
segmentId = segmentId.substr(segmentId.length - 16); // 16-digits
if (segmentId !== lastSegmentId) break;
}
lastSegmentId = segmentId;
return segmentId;
}
function getLambdaPrefix(annotations) {
// Prefix Lambda name by device ID.
return (annotations && annotations.device) // eslint-disable-next-line prefer-template
? annotations.device + '_@_'
: '';
}
function startTrace(/* req */) {
// Start the trace. Called by sigfoxCallback to start a trace.
console.log('startTrace - parentSegment', parentSegment);
// Create the child segment to represent sigfoxCallback.
if (parentSegment) {
const name = `${getLambdaPrefix(parentSegment.annotations)}${functionName}`;
const comment = `Run Lambda Func ${functionName}`;
childSegmentId = newTraceSegmentId();
childSegment = openTraceSegment(traceId, childSegmentId, parentSegmentId, name,
parentSegment.user, parentSegment.annotations, parentSegment.metadata, null, comment);
console.log('startTrace - childSegment:', childSegment);
}
const rootTraceStub = { // new tracingtrace(tracing, rootTraceId);
traceId: [traceId, parentSegmentId].join('|'),
startSpan: (/* rootSpanName, labels */) => rootSpanStub,
end: () => ({}),
};
const tracing = { startTrace: () => rootTraceStub };
return tracing.startTrace();
}
function createRootTrace(req, traceId0, traceSegment0) {
// Return the root trace for instrumentation. Called by
// non-sigfoxCallback (e.g. routeMessage) to continue a trace.
// We continue the trace passed by the previous Lambda and create a child segment.
if (traceSegment0) {
// Resume the receiver segment from the previous Lambda.
parentSegment = traceSegment0;
const name = `${getLambdaPrefix(parentSegment.annotations)}${functionName}`;
parentSegment.http.request.method += ` ${functionName}`;
parentSegment.name = name;
traceId = parentSegment.trace_id;
parentSegmentId = parentSegment.id;
sendTrace(req, parentSegment);
console.log('createRootTrace - parentSegment:', parentSegment);
}
// Create the child segment.
if (parentSegment) {
const name = `${getLambdaPrefix(parentSegment.annotations)}${functionName}`;
const comment = `Run Lambda Func ${functionName}`;
childSegmentId = newTraceSegmentId();
childSegment = openTraceSegment(traceId, childSegmentId, parentSegmentId, name,
parentSegment.user, parentSegment.annotations, parentSegment.metadata, null, comment);
console.log('createRootTrace - childSegment:', childSegment);
// Close the parent segment.
closeTraceSegment(parentSegment);
console.log('Close parentSegment', parentSegment);
parentSegment = null;
}
const rootTraceStub = {
traceId: [traceId, parentSegmentId].join('|'),
startSpan: (/* rootSpanName, labels */) => rootSpanStub,
end: () => ({}),
};
return rootTraceStub;
}
function initTrace(event, context) {
// During startup, create the trace segments.
const startTime = context.autoinstallStart; // Use autoinstall start time as start time.
const body = (typeof event.body === 'string') ? JSON.parse(event.body) : event.body;
const annotations = composeTraceAnnotations(body);
const metadata = getTraceMetadata(event);
const prefix = getLambdaPrefix(annotations);
console.log('initTrace', { body, annotations, metadata });
if (process.env._X_AMZN_TRACE_ID && !event.traceSegment) {
// This is the first Lambda in the chain, i.e. sigfoxCallback.
// For sigfoxCallback, we create a new segment and specify the URL.
// Set the environment for AWS XRay tracing based on a new trace ID.
// Get the trace ID from environment.
// _X_AMZN_TRACE_ID contains 'Root=1-5a24ba7c-4cfeb71c7b94c50c2f420a8c;Parent=6d0cb8bb50733c26;Sampled=1',
const fields = process.env._X_AMZN_TRACE_ID.split(';');
const parsedFields = {};
for (const field of fields) {
const fieldSplit = field.split('=');
const key = fieldSplit[0];
const val = fieldSplit[1];
parsedFields[key] = val;
}
traceId = parsedFields.Root;
const rootSegmentId = parsedFields.Parent;
// Create a new segment.
const comment = 'Receive message from Sigfox via HTTP POST Callback';
parentSegmentId = newTraceSegmentId();
parentSegment = openTraceSegment(traceId, parentSegmentId, rootSegmentId, prefix + functionName,
annotations.device, annotations, metadata, startTime, comment);
} else if (event.traceSegment) {
// This is the second or later Lambda in the chain, e.g. routeMessage, decodeStructuredMessage.
// Set the environment for AWS XRay tracing based on the traceSegment passed by previous Lambda.
// _X_AMZN_TRACE_ID will become 'Root=1-5a24ba7c-4cfeb71c7b94c50c2f420a8c;Parent=6d0cb8bb50733c26;Sampled=1',
parentSegment = JSON.parse(JSON.stringify(event.traceSegment));
traceId = parentSegment.trace_id;
parentSegmentId = parentSegment.id;
}
// Update the environment.
process.env._X_AMZN_TRACE_ID = `Root=${traceId};Parent=${parentSegmentId};Sampled=1`;
// Create a segment for autoinstall and close it.
let autoinstallSegment = null;
if (startTime) {
// name = 2C30EB_@_autoinstall_sigfoxCallback
const name = `${prefix}autoinstall_${functionName}`;
const comment = `Autoinstall modules for ${functionName}`;
autoinstallSegment = openTraceSegment(traceId, newTraceSegmentId(), parentSegmentId, name,
annotations.device, annotations, metadata, startTime, comment);
closeTraceSegment(autoinstallSegment);
}
console.log('initTrace parentSegment', parentSegment, '_X_AMZN_TRACE_ID', process.env._X_AMZN_TRACE_ID,
{ autoinstallSegment });
}
// //////////////////////////////////////////////////////////////////////////////////// endregion
// region File Functions: Store and retrieve files from AWS S3 storage
const s3 = new AWS.S3();
function writeFile(req, bucket, name, obj) {
// Write file to S3 bucket. Serialise the object to JSON. Returns a promise.
const params = {
Body: JSON.stringify(obj, null, 2),
Bucket: bucket,
Key: name,
};
return s3.putObject(params).promise()
.catch((error) => {
module.exports.error(req, 'writeFile', { error, bucket, name });
throw error;
});
}
function readFile(req, bucket, name) {
// Read file from S3 bucket. Returns a promise for a JavaScript object.
// Return null if not found.
const params = {
Bucket: bucket,
Key: name,
};
return s3.getObject(params).promise()
.then(res => (res && res.Body) ? JSON.parse(res.Body) : null)
.catch(() => null);
}
function deleteFile(req, bucket, name) {
// Delete file from S3 bucket. Returns a promise.
const params = {
Bucket: bucket,
Key: name,
};
return s3.deleteObject(params).promise()
.catch((error) => {
module.exports.error(req, 'deleteFile', { error, bucket, name });
throw error;
});
}
// //////////////////////////////////////////////////////////////////////////////////// endregion
// region Logging Functions: Log to AWS CloudWatch
// Logger object for AWS.
const loggingLog = {
write: (/* entry */) => {
// Write the log entry to AWS CloudWatch.
// console.log(stringify(entry ? entry.event || '' : '', null, 2));
return Promise.resolve({});
},
entry: (metadata, event) => {
// Create the log event.
console.log(JSON.stringify(event, null, 2));
return ({ metadata, event });
},
};
/* metadata looks like {
timestamp: '2017-11-25T14:10:37.669Z',
severity: 'DEBUG',
operation: {
id: 'saveMessage_1037-3e363ed3-e368-4013-9776-41cd3392f461',
producer: 'unabiz.com',
first: true,
last: false,
},
resource: {
type: 'cloud_function',
labels: {function_name: 'sigfoxCallback'},
}};
event looks like {
'____[ 1A2345 ]____saveMessage___________': {
device: '1A2345',
body: {
uuid: 'df0cbceb-00f3-4be2-add1-a32ffdee9773',
datetime: '2017-11-25 14:10:37',
localdatetime: '2017-11-25 22:10:37',
callbackTimestamp: 1511619037666,
device: '1A2345',
data: 'b0513801a421f0019405a500',
duplicate: false,
snr: 18.86,
station: '1D44',
avgSnr: 15.54,
lat: 1,
lng: 104,
rssi: -123,
seqNumber: 1508,
ack: false,
longPolling: false,
timestamp: '1511814827000',
baseStationTime: 1511814827,
},
duration: 0,
}}; */
function getLogger() {
// Return the logger object for writing logs.
return loggingLog;
}
function reportError(/* req. err, action, para */) {
// TODO: Report error to CloudWatch.
}
// //////////////////////////////////////////////////////////////////////////////////// endregion
// region Metadata Functions: Read function metadata from environment
function authorizeFunctionMetadata(/* req */) {
// Authorize access to function metadata. On AWS do nothing.
return Promise.resolve({ result: 'OK' });
}
function getFunctionMetadata(/* req, authClient */) {
// Returns a promise for function metadata keys and values: { key1: val1, key2: val2, ... }
// In lieu of the metadata store, we read from the environment variables.
// This is done in sigfox-iot-cloud.getMetadata.
return Promise.resolve({});
}
// //////////////////////////////////////////////////////////////////////////////////// endregion
// region Messaging Functions: Dispatch messages between Cloud Functions via AWS IoT MQTT Queues
const Iot = new AWS.Iot();
let awsIoTDataPromise = null;
function createQueueSegment(req, topic, payloadObj) {
// Create the 3 child trace segments (sender, rule and receiver segments) for the outgoing message.
// Pass the receiver segment through traceSegment in the message.
// Write the 3 segments to S3 storage so that processIoTLogs can match up with AWS IoT log and open/close the
// segments.
if (!childSegment) return null;
const annotations = composeTraceAnnotations(payloadObj);
const metadata = getTraceMetadata(payloadObj) || {};
const device = payloadObj.device || payloadObj.body.device || '';
const name = `==_${device}_@_${topic}_==`;
const comment = `Send message to MQTT queue ${topic}`;
const startTime = Date.now();
metadata.startTime = startTime;
metadata.comment = comment;
// Create 3 segments but send only the first one: sender, rule, receiver.
const senderSegment = openTraceSegment(traceId, newTraceSegmentId(), childSegmentId, name, device, annotations, metadata,
startTime, comment);
const ruleSegment = openTraceSegment(traceId, newTraceSegmentId(), senderSegment.id, 'ruleSegment', device, annotations, metadata,
startTime + 40, 'Apply rule with matching conditions');
const receiverSegment = createTraceSegment(traceId, newTraceSegmentId(), ruleSegment.id, 'receiverSegment', device, annotations, metadata,
startTime + 80, 'Trigger rule action to run Lambda Func');
// Pass the receiver segment to the payload.
/* eslint-disable no-param-reassign */
payloadObj.traceSegment = receiverSegment;
payloadObj.rootTraceId = [traceId, receiverSegment.id].join('|'); // For info, not really used.
/* eslint-enable no-param-reassign */
// Send the message to the trace queue for processIoTLogs to match up AWS IoT Rules and Lambda invocations.
// The trace topic looks like sigfox/trace/<deviceid>-<sendersegmentid>
const traceName = `${device}-${senderSegment.id}`;
const traceTopic = `sigfox/trace/${traceName}`;
if (process.env.TRACE_BUCKET) {
// Save the 3 segments into trace file "segment-<deviceid>-<sendersegmentid>.json" for processIoTLogs to retrieve and match later.
writeFile(req, process.env.TRACE_BUCKET, `segment-${traceName}.json`, {
senderSegment, ruleSegment, receiverSegment,
})
.catch(error => console.error('createQueueSegment', error.message, error.stack));
}
console.log('createQueueSegment - segment:', senderSegment, traceTopic);
return traceTopic;
}
function sendIoTMessage(req, topic0, payload0) {
// Send the text message to the AWS IoT MQTT queue name.
// In Google Cloud topics are named like sigfox.devices.all. We need to rename them
// to AWS MQTT format like sigfox/devices/all.
const topic = (topic0 || '').split('.').join('/');
// We inject a segment for the queue, e.g. ==_sigfox/types/routeMessage_==
const payloadObj = JSON.parse(payload0);
const traceTopic = createQueueSegment(req, topic, payloadObj);
const payload = JSON.stringify(payloadObj);
// Send the message to AWS IoT MQTT queue.
const params = { topic, payload, qos: 0 };
// Send the message to the begin and end trace queues for processIoTLogs to match up AWS IoT Rules and Lambda invocations.
const beginTrace = traceTopic ? { topic: `${traceTopic}/begin`, payload: '{}', qos: 0 } : null;
const endTrace = traceTopic ? { topic: `${traceTopic}/end`, payload: '{}', qos: 0 } : null;
let IotData = null;
let result = null;
module.exports.log(req, 'sendIoTMessage', { topic, payloadObj, params, beginTrace, endTrace }); // eslint-disable-next-line no-use-before-define
return getIoTData(req)
.then((res) => { IotData = res; })
// Send begin trace message.
.then(() => (beginTrace === null) || IotData.publish(beginTrace).promise() // Ignore any trace errors.
.catch(error => console.error('begin trace', error.message, error.stack)))
// Send actual message.
.then(() => IotData.publish(params).promise())
.then((res) => { result = res; })
// Send end trace message.
.then(() => (endTrace === null) || IotData.publish(endTrace).promise() // Ignore any trace errors.
.catch(error => console.error('end trace', error.message, error.stack)))
.then(() => {
module.exports.log(req, 'sendIoTMessage', { result, topic, payloadObj, params });
return result;
})
.catch((error) => { module.exports.error(req, 'sendIoTMessage', { error, topic, payloadObj, params }); throw error; });
}
/* function sendSQSMessage(req, topic0, msg) {
// Send the text message to the AWS Simple Queue Service queue name.
// In Google Cloud topics are named like sigfox.devices.all. We need to rename them
// to AWS SQS format like sigfox-devices-all.
const msgObj = JSON.parse(msg);
const topic = (topic0 || '').split('.').join('-');
const url = `${SQS.endpoint.href}${topic}`;
const params = {
MessageBody: msg,
QueueUrl: url,
DelaySeconds: 0,
MessageAttributes: {
device: {
DataType: 'String',
StringValue: msgObj.device || 'missing_device',
},
},
};
module.exports.log(req, 'awsSendSQSMessage', { topic, url, msgObj, params });
return SQS.sendMessage(params).promise()
.then(result => module.exports.log(req, 'awsSendSQSMessage', { result, topic, url, msgObj, params }))
.catch((error) => { module.exports.error(req, 'awsSendSQSMessage', { error, topic, url, msgObj, params }); throw error; });
} */
function getQueue(req, projectId0, topicName) {
// Return the AWS IoT MQTT Queue and AWS Simple Queue Service queue with that name
// for that project. Will be used for publishing messages, not reading.
const topic = {
name: topicName,
publisher: () => ({
// Calling publish on this queue will send an AWS IoT MQTT message.
publish: buffer => sendIoTMessage(req, topicName, buffer.toString())
.catch(error => console.error('getQueue', error.message, error.stack)),
}),
};
return topic;
}
// //////////////////////////////////////////////////////////////////////////////////// endregion
// region Device State Functions: Memorise the device state with AWS IoT Thing Shadows
function getIoTData(/* req */) {
// Return a promise for the IotData object for updating message queue
// and device state.
if (awsIoTDataPromise) return awsIoTDataPromise;
awsIoTDataPromise = Iot.describeEndpoint({}).promise()
.then((res) => {
const IotData = new AWS.IotData({ endpoint: res.endpointAddress });
return IotData;
})
.catch((error) => {
awsIoTDataPromise = null;
throw error;
});
return awsIoTDataPromise;
}
function createDevice(req, device0) {
// Create the AWS Thing with the device name if it doesn't exist. device is the
// Sigfox device ID.
if (!device0) throw new Error('missing_deviceid');
// Capitalise device ID but not device names.
const device = device0.length > 6 ? device0 : device0.toUpperCase();
const params = { thingName: device };
console.log({ describeThing: params });
// Lookup the device.
return Iot.describeThing(params).promise()
// Device exists.
.then(result => module.exports.log(req, 'awsCreateDevice', { result, device, params }))
// Device is missing. Create it.
.catch(() => console.log({ createThing: params }) || Promise.resolve(null)
.then(() => Iot.createThing(params).promise())
.then(result => module.exports.log(req, 'awsCreateDevice', { result, device, params }))
.catch((error) => { module.exports.error(req, 'awsCreateDevice', { error, device, params }); throw error; }));
}
function getDeviceState(req, device0) {
// Fetch the AWS IoT Thing state for the device ID. Returns a promise.
// Result looks like {"reported":{"deviceLat":1.303224739957452,...
if (!device0) throw new Error('missing_deviceid');
// Capitalise device ID but not device names.
const device = device0.length > 6 ? device0 : device0.toUpperCase();
const params = { thingName: device };
console.log({ getThingShadow: params });
// Get a connection for AWS IoT Data.
return getIoTData(req)
// Fetch the Thing state.
.then(IotData => IotData.getThingShadow(params).promise())
// Return the payload.state.
.then(res => (res && res.payload) ? JSON.parse(res.payload) : res)
.then(res => (res && res.state) ? res.state : res)
.then(result => module.exports.log(req, 'awsGetDeviceState', { result, device, params }))
.catch((error) => { module.exports.error(req, 'awsGetDeviceState', { error, device, params }); throw error; });
}
// eslint-disable-next-line no-unused-vars
function updateDeviceState(req, device0, state0) {
// Update the AWS IoT Thing state for the device ID. Returns a promise.
// Overwrites the existing Thing attributes with the same name.
if (!device0) throw new Error('missing_deviceid');
// Capitalise device ID but not device names.
const device = device0.length > 6 ? device0 : device0.toUpperCase();
// AWS allows only max 6 levels for device state. We truncate beyond 6 levels.
const state = module.exports.removeNulls(state0, -2);
const payload = {
state: {
reported: state,
},
};
const params = {
payload: JSON.stringify(payload),
thingName: device,
};
console.log({ updateThingShadow: params });
// Get a connection for AWS IoT Data.
return getIoTData(req)
// Update the Thing state.
.then(IotData => IotData.updateThingShadow(params).promise())
.then(result => module.exports.log(req, 'awsUpdateDeviceState', { result, device, state, payload, params }))
.catch((error) => { module.exports.error(req, 'awsUpdateDeviceState', { error, device, state, payload, params }); throw error; });
}
// //////////////////////////////////////////////////////////////////////////////////// endregion
// region Startup
function prepareRequest(event, context) {
// Prepare the request object and return it.
const body = (typeof event.body === 'string')
? JSON.parse(event.body) // For HTTP request.
: null; // For queue requests.
return { body, returnStatus: null, returnJSON: null, requestId: context ? context.awsRequestId : null };
}
/* body looks like {
device: '1A2345',
data: 'b0513801a421f0019405a500',
time: '1507112763',
duplicate: 'false',
snr: '18.86',
station: '1D44',
avgSnr: '15.54',
lat: '1',
lng: '104',
rssi: '-123.00',
seqNumber: '1508',
ack: 'false',
longPolling: 'false',
}; */
function done(req, error, result, statusCode0, callback) {
// Return a statusCode and JSON response to the HTTP request. If error is set return the error
// else return the result. If statusCode is null,
// return 200 or 500 depending on where the error
// is absent or present.
return callback(null, {
statusCode: statusCode0 || (error ? 500 : 200),
body: error ? error.message : JSON.stringify(result),
headers: {
'Content-Type': 'application/json',
},
});
}
function init(event, context, callback, task) {
// Run the function in the wrapper, passed as "this".
// Call the callback upon success or failure.
// Returns a promise.
console.log('init', { event, context, callback, task, env: process.env });
// Generate a random prefix for the AWS XRay segment ID.
segmentPrefix = Math.floor(Math.random() * 10000).toString(16);
// Create the segments for AWS XRay tracing.
initTrace(event, context);
// This tells AWS to quit as soon as we call callback. Else AWS will wait
// for all functions to stop running. This causes some background functions
// to hang e.g. the knex library in sigfox-aws-data. Also this setting allows us
// to cache variables across Lambda invocations.
// eslint-disable-next-line no-param-reassign
context.callbackWaitsForEmptyEventLoop = false;
// Prepare the request and result objects.
const req = prepareRequest(event, context);
// Result object that wii be passed to wrapper.
const res = { // Simulates some functions of the ExpressJS Response object.
status: (code) => {
// Return HTTP response code.
req.returnStatus = code;
return res;
},
json: (obj) => {
// Return HTTP response JSON.
req.returnJSON = obj;
return res;
},
end: () => {
// End the request. We return the response code and JSON.
const error = null;
done(req, error, req.returnJSON, req.returnStatus, callback);
return res;
},
};
req.res = res; // Save the response object in the request for easy reference.
const result = { req, res };
if (event) result.event = event;
if (context) result.context = context;
if (callback) {
// Save the callback for use in shutdown().
req.callback = callback;
result.callback = callback;
}
if (task) result.task = task;
return result;
}
function shutdown(req, useCallback, error, result) {
// Close all cloud connections. If useCallback is true, return the error or result
// to AWS through the callback.
const promises = [];
if (childSegment) {
promises.push(closeTraceSegment(childSegment)
.then((res) => { console.log('Close childSegment', res, childSegment); childSegment = null; return res; })
.catch(err => console.error('shutdown child', err.message, err.stack)));
}
if (parentSegment) {
promises.push(closeTraceSegment(parentSegment)
.then((res) => { console.log('Close parentSegment', res, parentSegment); parentSegment = null; return res; })
.catch(err => console.error('shutdown parent', err.message, err.stack)));
}
return Promise.all(promises)
.then((res) => {
console.log('shutdown', res);
if (useCallback) { // useCallback is normally true except for sigfoxCallback.
const callback = req.callback;
if (callback && typeof callback === 'function') {
return Promise.resolve(callback(error, result));
}
}
return Promise.resolve(error || result);
})
.catch(err => console.error('shutdown', err.message, err.stack));
}
// //////////////////////////////////////////////////////////////////////////////////// endregion
// region Module Exports
// Here are the functions specific to AWS. We will expose the sigfox-iot-cloud interface which is common to Google Cloud and AWS.
const cloud = {
isGoogleCloud,
isAWS,
projectId: null,
functionName,
logName,
sourceName: process.env.AWS_LAMBDA_FUNCTION_NAME || logName,
credentials: null, // No credentials needed.
// Logging
getLogger,
reportError,
// Instrumentation
startTrace,
createRootTrace,
sendTrace,
// File
readFile,
writeFile,
deleteFile,
// Messaging
getQueue,
// Metadata
authorizeFunctionMetadata,
getFunctionMetadata,
// Device State
createDevice,
getDeviceState,
updateDeviceState,
// Startup
init,
shutdown,
};
// Functions common to Google Cloud and AWS are exposed here. So clients of both clouds will see the same interface.
module.exports = require('sigfox-iot-cloud')(cloud);
// For Unit Test
module.exports.getAWSXRay = () => AWSXRay;
module.exports.getAWS = () => AWS;
// //////////////////////////////////////////////////////////////////////////////////// endregion