/**
* Interception module for logging usage, performance and error information about data provider methods.
*
* By default the interceptor uses [winston](https://github.com/flatiron/winston) library for logging. Winston can be configured as you would normally do.
* Please see **winston** documentation. To set custom logger, use `options.logger` property.
*
* For more details on using interceptors please see the {@tutorial interceptors} tutorial.
*
* @example
* var entree = require("entree"),
* log = entree.resolveInterceptor("log"),
* opts = {
* level: "info",
* log: {
* action: true,
* query: true
* }
* };
*
* entree.posts.use(log.interception(opts, ["insert", "update", "delete"]));
*
* @example
* var options = {
* logger: require("path/to/logger"); // or an instance of winston logger
* };
*
* provider.use(log.interception(options));
*
* @module log
*/
"use strict";
var util = require("util"),
_ = require("lodash"),
wrapFns = ["toArray", "next", "each", "count", "first", "update", "delete"];
function convertError(err) {
if (util.isError(err)) {
var obj = {};
Object.getOwnPropertyNames(err).forEach(function (key) {
obj[key] = err[key];
});
return obj;
}
return err;
}
/**
* Configures and returns an interceptor function.
*
* Options
*
* - `logger` {Object}, logger instance, if omitted default **winston** instance is used.
* - `level` {String}, logging level for actions in normal flow. Default is "info".
* - `errorLevel` {String}, logging level for errors. Default is "error".
* - `log` {Object}, defines what type of information should be logged. By default only actions are logged.
* - `action` {Boolean}, logs the name of the intercepted method. Default is true.
* - `profile` {Boolean}, profiles, logs execution duration, for all intercepted methods in milliseconds. Default is false.
* - `query` {Boolean}, logs the "query" argument of the intercepted method as metadata. Default is false.
* - `context` {Boolean}, logs the "context" argument of the intercepted method as metadata. Default is false.
* - `result` {Boolean}, logs the result returned by the intercepted method as metadata. NOTE: This is potentially dangerous option for select method if the returned result set is very large. Default is false.
* - `error` {Boolean}, logs error message if an error was returned by the intercepted method. Default is false.
*
* @param {object=} opts - Logger options.
* @param {(string|string[])=} actions - Specifies which actions should be logged. Could be single action or an array of actions. If omitted, all actions are logged.
* Possible values are: `["insert", "upsert", "update", "get", "delete", "select"]`
* @return {function}
*/
exports.interception = function (opts, actions) {
if (!opts) {
opts = {};
}
var logger = opts.logger || require("winston"),
level = opts.level || "info",
errorLevel = opts.errorLevel || "error",
log = opts.log || { action: true },
tracking,
i;
if (actions) {
if (!_.isArray(actions)) {
actions = [actions];
}
for (i = 0; i < actions.length; i++) {
if (actions[i].charAt(0) !== "_") {
actions[i] = "_" + actions[i];
}
}
}
return function (action, context, item, next, out) {
var msg = this.name + "." + action,
meta;
function setMeta(prop, val) {
if (!meta) {
meta = {};
}
meta[prop] = val;
}
function logMessage(prop, val, lvl) {
if (prop && val) {
setMeta(prop, val);
}
if (log.action || meta) {
if (meta) {
logger.log(lvl || level, msg, meta);
} else {
logger.log(lvl || level, msg);
}
}
}
function logError(err) {
if (err && log.error) {
logMessage("error", convertError(err), errorLevel);
}
}
function handleResponse(err, res, profKey) {
if (log.profile) {
logger.profile(profKey || msg);
}
if (err) {
logError(err);
} else {
if (log.result) {
logMessage("result", res);
} else {
logMessage();
}
}
}
function replaceFn(cursor, orgFn, name) {
return function (data, callback) {
if (!callback) {
callback = data;
data = null;
}
function wrapper(err, res) {
if (name !== "each" || !res) {
var profKey = msg;
msg += "." + name;
handleResponse(err, res, profKey);
}
callback(err, res);
}
if (tracking) {
if (tracking !== name) {
if (data) {
return orgFn.call(cursor, data, callback);
}
return orgFn.call(cursor, callback);
}
} else {
tracking = name;
}
if (data) {
orgFn.call(cursor, data, wrapper);
} else {
orgFn.call(cursor, wrapper);
}
};
}
function wrapCursor(cursor) {
var i;
for (i = 0; i < wrapFns.length; i++) {
var name = wrapFns[i];
cursor[name] = replaceFn(cursor, cursor[name], name);
}
}
if (tracking || (actions && actions.indexOf(action) === -1)) {
return next(item, out);
}
if (log.profile) {
logger.profile(msg);
}
if (log.query) {
meta = { query: item };
}
if (log.context) {
setMeta("context", context || null);
}
if (log.result || log.error || log.profile) {
if (out) {
next(item, function (err, res) {
if (action === "_select") {
wrapCursor(res);
} else {
handleResponse(err, res);
}
out(err, res);
});
} else {
var cur;
try {
cur = next(item);
} catch (e) {
logError(e);
throw e;
}
if (cur) {
wrapCursor(cur);
}
return cur;
}
} else {
logMessage();
return next(item, out);
}
};
};