lambda-api
Version:
Lightweight web framework for your serverless applications
372 lines (323 loc) • 10.5 kB
JavaScript
;
/**
* Lightweight web framework for your serverless applications
* @author Jeremy Daly <jeremy@jeremydaly.com>
* @license MIT
*/
// IDEA: add unique function identifier
// IDEA: response length
// https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-mapping-template-reference.html#context-variable-reference
const UTILS = require('./utils'); // Require utils library
const { ConfigurationError } = require('./errors'); // Require custom errors
// Config logger
exports.config = (config, levels) => {
let cfg = config ? config : {};
// Add custom logging levels
if (cfg.levels && typeof cfg.levels === 'object') {
for (let lvl in cfg.levels) {
if (!/^[A-Za-z_]\w*$/.test(lvl) || isNaN(cfg.levels[lvl])) {
throw new ConfigurationError('Invalid level configuration');
}
}
levels = Object.assign(levels, cfg.levels);
}
// Configure sampling rules
let sampling = cfg.sampling
? parseSamplerConfig(cfg.sampling, levels)
: false;
// Parse/default the logging level
let level =
cfg === true
? 'info'
: cfg.level && levels[cfg.level.toLowerCase()]
? cfg.level.toLowerCase()
: cfg.level === 'none'
? 'none'
: Object.keys(cfg).length > 0
? 'info'
: 'none';
let messageKey =
cfg.messageKey && typeof cfg.messageKey === 'string'
? cfg.messageKey.trim()
: 'msg';
let customKey =
cfg.customKey && typeof cfg.customKey === 'string'
? cfg.customKey.trim()
: 'custom';
let timestamp =
cfg.timestamp === false
? () => undefined
: typeof cfg.timestamp === 'function'
? cfg.timestamp
: () => Date.now();
let timer =
cfg.timer === false ? () => undefined : (start) => Date.now() - start;
let nested = cfg.nested === true ? true : false; // nest serializers
let stack = cfg.stack === true ? true : false; // show stack traces in errors
let access =
cfg.access === true ? true : cfg.access === 'never' ? 'never' : false; // create access logs
let detail = cfg.detail === true ? true : false; // add req/res detail to all logs
let multiValue = cfg.multiValue === true ? true : false; // return qs as multiValue
let defaults = {
req: (req) => {
return {
path: req.path,
ip: req.ip,
ua: req.userAgent,
device: req.clientType,
country: req.clientCountry,
version: req.version,
qs: multiValue
? Object.keys(req.multiValueQuery).length > 0
? req.multiValueQuery
: undefined
: Object.keys(req.query).length > 0
? req.query
: undefined,
};
},
res: () => {
return {};
},
context: (context) => {
return {
remaining:
context.getRemainingTimeInMillis &&
context.getRemainingTimeInMillis(),
function: context.functionName && context.functionName,
memory: context.memoryLimitInMB && context.memoryLimitInMB,
};
},
custom: (custom) =>
(typeof custom === 'object' && !Array.isArray(custom)) || nested
? custom
: { [customKey]: custom },
};
let serializers = {
main:
cfg.serializers && typeof cfg.serializers.main === 'function'
? cfg.serializers.main
: () => {},
req:
cfg.serializers && typeof cfg.serializers.req === 'function'
? cfg.serializers.req
: () => {},
res:
cfg.serializers && typeof cfg.serializers.res === 'function'
? cfg.serializers.res
: () => {},
context:
cfg.serializers && typeof cfg.serializers.context === 'function'
? cfg.serializers.context
: () => {},
custom:
cfg.serializers && typeof cfg.serializers.custom === 'function'
? cfg.serializers.custom
: () => {},
};
// Overridable logging function
let logger =
cfg.log && typeof cfg.log === 'function'
? cfg.log
: (...a) => console.log(...a); // eslint-disable-line no-console
// Main logging function
let log = (level, msg, req, context, custom) => {
let _context = Object.assign(
{},
defaults.context(context),
serializers.context(context)
);
let _custom =
typeof custom === 'object' && !Array.isArray(custom)
? Object.assign({}, defaults.custom(custom), serializers.custom(custom))
: defaults.custom(custom);
return Object.assign(
{},
{
level,
time: timestamp(),
id: req.id,
route: req.route,
method: req.method,
[messageKey]: msg,
timer: timer(req._start),
int: req.interface,
sample: req._sample ? true : undefined,
},
serializers.main(req),
nested ? { [customKey]: _custom } : _custom,
nested ? { context: _context } : _context
);
}; // end log
// Formatting function for additional log data enrichment
let format = function (info, req, res) {
let _req = Object.assign({}, defaults.req(req), serializers.req(req));
let _res = Object.assign({}, defaults.res(res), serializers.res(res));
return Object.assign(
{},
info,
nested ? { req: _req } : _req,
nested ? { res: _res } : _res
);
}; // end format
// Return logger object
return {
level,
stack,
logger,
log,
format,
access,
detail,
sampling,
errorLogging: cfg.errorLogging !== false,
};
};
// Determine if we should sample this request
exports.sampler = (app, req) => {
if (app._logger.sampling) {
// Default level to false
let level = false;
// Create local reference to the rulesMap
let map = app._logger.sampling.rulesMap;
// Parse the current route
let route = UTILS.parsePath(req.route);
// Default wildcard mapping
let wildcard = {};
// Loop the map and see if this route matches
route.forEach((part) => {
// Capture wildcard mappings
if (map['*']) wildcard = map['*'];
// Traverse map
map = map[part] ? map[part] : {};
}); // end for loop
// Set rule reference based on route
let ref =
typeof map['__' + req.method] === 'number'
? map['__' + req.method]
: typeof map['__ANY'] === 'number'
? map['__ANY']
: typeof wildcard['__' + req.method] === 'number'
? wildcard['__' + req.method]
: typeof wildcard['__ANY'] === 'number'
? wildcard['__ANY']
: -1;
let rule =
ref >= 0
? app._logger.sampling.rules[ref]
: app._logger.sampling.defaults;
// Assign rule reference to the REQUEST
req._sampleRule = rule;
// Get last sample time (default start, last, fixed count, period count and total count)
let counts =
app._sampleCounts[rule.default ? 'default' : req.route] ||
Object.assign(app._sampleCounts, {
[rule.default ? 'default' : req.route]: {
start: 0,
fCount: 0,
pCount: 0,
tCount: 0,
},
})[rule.default ? 'default' : req.route];
let now = Date.now();
// Calculate the current velocity
let velocity =
rule.rate > 0
? (rule.period * 1000) /
((counts.tCount / (now - app._initTime)) *
rule.period *
1000 *
rule.rate)
: 0;
// If this is a new period, reset values
if (now - counts.start > rule.period * 1000) {
counts.start = now;
counts.pCount = 0;
// If a rule target is set, sample the start
if (rule.target > 0) {
counts.fCount = 1;
level = rule.level; // set the sample level
// console.log('\n*********** NEW PERIOD ***********');
}
// Enable sampling if last sample is passed target split
} else if (
rule.target > 0 &&
counts.start +
Math.floor(((rule.period * 1000) / rule.target) * counts.fCount) <
now
) {
level = rule.level;
counts.fCount++;
// console.log('\n*********** FIXED ***********');
} else if (
rule.rate > 0 &&
counts.start + Math.floor(velocity * counts.pCount + velocity / 2) < now
) {
level = rule.level;
counts.pCount++;
// console.log('\n*********** RATE ***********');
}
// Increment total count
counts.tCount++;
return level;
} // end if sampling
return false;
};
// Parse sampler configuration
const parseSamplerConfig = (config, levels) => {
// Default config
let cfg = typeof config === 'object' ? config : config === true ? {} : false;
// Error on invalid config
if (cfg === false)
throw new ConfigurationError('Invalid sampler configuration');
// Create rule default
let defaults = (inputs) => {
return {
// target, rate, period, method, level
target: Number.isInteger(inputs.target) ? inputs.target : 1,
rate: !isNaN(inputs.rate) && inputs.rate <= 1 ? inputs.rate : 0.1,
period: Number.isInteger(inputs.period) ? inputs.period : 60, // in seconds
level: Object.keys(levels).includes(inputs.level)
? inputs.level
: 'trace',
};
};
// Init ruleMap
let rulesMap = {};
// Parse and default rules
let rules = Array.isArray(cfg.rules)
? cfg.rules.map((rule, i) => {
// Error if missing route or not a string
if (!rule.route || typeof rule.route !== 'string')
throw new ConfigurationError('Invalid route specified in rule');
// Parse methods into array (if not already)
let methods = (
Array.isArray(rule.method)
? rule.method
: typeof rule.method === 'string'
? rule.method.split(',')
: ['ANY']
).map((x) => x.toString().trim().toUpperCase());
let map = {};
let recursive = map; // create recursive reference
UTILS.parsePath(rule.route).forEach((part) => {
Object.assign(recursive, { [part === '' ? '/' : part]: {} });
recursive = recursive[part === '' ? '/' : part];
});
Object.assign(
recursive,
methods.reduce((acc, method) => {
return Object.assign(acc, { ['__' + method]: i });
}, {})
);
// Deep merge the maps
UTILS.deepMerge(rulesMap, map);
return defaults(rule);
}, {})
: {};
return {
defaults: Object.assign(defaults(cfg), { default: true }),
rules,
rulesMap,
};
}; // end parseSamplerConfig