Files
kami-parse-server/src/Controllers/LoggerController.js

244 lines
6.0 KiB
JavaScript

import { Parse } from 'parse/node';
import AdaptableController from './AdaptableController';
import { LoggerAdapter } from '../Adapters/Logger/LoggerAdapter';
const MILLISECONDS_IN_A_DAY = 24 * 60 * 60 * 1000;
const LOG_STRING_TRUNCATE_LENGTH = 1000;
const truncationMarker = '... (truncated)';
export const LogLevel = {
INFO: 'info',
ERROR: 'error',
};
export const LogOrder = {
DESCENDING: 'desc',
ASCENDING: 'asc',
};
export const logLevels = ['error', 'warn', 'info', 'debug', 'verbose', 'silly'];
export class LoggerController extends AdaptableController {
constructor(adapter, appId, options = { logLevel: 'info' }) {
super(adapter, appId, options);
let level = 'info';
if (options.verbose) {
level = 'verbose';
}
if (options.logLevel) {
level = options.logLevel;
}
const index = logLevels.indexOf(level); // info by default
logLevels.forEach((level, levelIndex) => {
if (levelIndex > index) {
// silence the levels that are > maxIndex
this[level] = () => {};
}
});
}
maskSensitiveUrl(path) {
const urlString = 'http://localhost' + path; // prepend dummy string to make a real URL
const urlObj = new URL(urlString);
const query = urlObj.searchParams;
let sanitizedQuery = '?';
for (const [key, value] of query) {
if (key !== 'password') {
// normal value
sanitizedQuery += key + '=' + value + '&';
} else {
// password value, redact it
sanitizedQuery += key + '=' + '********' + '&';
}
}
// trim last character, ? or &
sanitizedQuery = sanitizedQuery.slice(0, -1);
// return original path name with sanitized params attached
return urlObj.pathname + sanitizedQuery;
}
maskSensitive(argArray) {
return argArray.map(e => {
if (!e) {
return e;
}
if (typeof e === 'string') {
return e.replace(/(password".?:.?")[^"]*"/g, '$1********"');
}
// else it is an object...
// check the url
if (e.url) {
// for strings
if (typeof e.url === 'string') {
e.url = this.maskSensitiveUrl(e.url);
} else if (Array.isArray(e.url)) {
// for strings in array
e.url = e.url.map(item => {
if (typeof item === 'string') {
return this.maskSensitiveUrl(item);
}
return item;
});
}
}
if (e.body) {
for (const key of Object.keys(e.body)) {
if (key === 'password') {
e.body[key] = '********';
break;
}
}
}
if (e.params) {
for (const key of Object.keys(e.params)) {
if (key === 'password') {
e.params[key] = '********';
break;
}
}
}
return e;
});
}
log(level, args) {
// make the passed in arguments object an array with the spread operator
args = this.maskSensitive([...args]);
args = [].concat(
level,
args.map(arg => {
if (typeof arg === 'function') {
return arg();
}
return arg;
})
);
this.adapter.log.apply(this.adapter, args);
}
info() {
return this.log('info', arguments);
}
error() {
return this.log('error', arguments);
}
warn() {
return this.log('warn', arguments);
}
verbose() {
return this.log('verbose', arguments);
}
debug() {
return this.log('debug', arguments);
}
silly() {
return this.log('silly', arguments);
}
logRequest({ method, url, headers, body }) {
this.verbose(
() => {
const stringifiedBody = JSON.stringify(body, null, 2);
return `REQUEST for [${method}] ${url}: ${stringifiedBody}`;
},
{
method,
url,
headers,
body,
}
);
}
logResponse({ method, url, result }) {
this.verbose(
() => {
const stringifiedResponse = JSON.stringify(result, null, 2);
return `RESPONSE from [${method}] ${url}: ${stringifiedResponse}`;
},
{ result: result }
);
}
// check that date input is valid
static validDateTime(date) {
if (!date) {
return null;
}
date = new Date(date);
if (!isNaN(date.getTime())) {
return date;
}
return null;
}
truncateLogMessage(string) {
if (string && string.length > LOG_STRING_TRUNCATE_LENGTH) {
const truncated = string.substring(0, LOG_STRING_TRUNCATE_LENGTH) + truncationMarker;
return truncated;
}
return string;
}
static parseOptions(options = {}) {
const from =
LoggerController.validDateTime(options.from) ||
new Date(Date.now() - 7 * MILLISECONDS_IN_A_DAY);
const until = LoggerController.validDateTime(options.until) || new Date();
const size = Number(options.size) || 10;
const order = options.order || LogOrder.DESCENDING;
const level = options.level || LogLevel.INFO;
return {
from,
until,
size,
order,
level,
};
}
// Returns a promise for a {response} object.
// query params:
// level (optional) Level of logging you want to query for (info || error)
// from (optional) Start time for the search. Defaults to 1 week ago.
// until (optional) End time for the search. Defaults to current time.
// order (optional) Direction of results returned, either “asc” or “desc”. Defaults to “desc”.
// size (optional) Number of rows returned by search. Defaults to 10
getLogs(options = {}) {
if (!this.adapter) {
throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED, 'Logger adapter is not available');
}
if (typeof this.adapter.query !== 'function') {
throw new Parse.Error(
Parse.Error.PUSH_MISCONFIGURED,
'Querying logs is not supported with this adapter'
);
}
options = LoggerController.parseOptions(options);
return this.adapter.query(options);
}
expectedAdapterType() {
return LoggerAdapter;
}
}
export default LoggerController;