Refactor logging to provide common logger from LoggerAdapter (#2478)

* Refactor logging to provide common logger from LoggerAdapter

Move logger logic de WinstonLoggerAdapter

Further improvements in configuration

Use logger instead of getLogger

- Removes PLog module

Reverts name changes

nits

* Adds additional logging levels as requirements

* Adds tests for logging configuration

* removes flaky test

* investigate...

* further investigation

* Adds silent option to disable console output

* Restores logs with VERBOSE in tests

* Expose controller instead of adapter, reduces method requirements for adapter

* Shuffles initializations around

* Fix doc

* Load cloudCode last to make sure the logger is available

* Adds test to make sure we can load an adapter from npm module

* extract defaults

* Adds defaultMongoURI to defaults

* fix defaults values

* Proper error for PG failures

* Disable flaky test
This commit is contained in:
Florent Vilmart
2016-08-12 13:25:24 -04:00
committed by Drew
parent 6e0a25dea0
commit a5a172918e
28 changed files with 396 additions and 293 deletions

View File

@@ -8,14 +8,13 @@
import { MongoClient, GridStore, Db} from 'mongodb';
import { FilesAdapter } from './FilesAdapter';
const DefaultMongoURI = 'mongodb://localhost:27017/parse';
import defaults from '../../defaults';
export class GridStoreAdapter extends FilesAdapter {
_databaseURI: string;
_connectionPromise: Promise<Db>;
constructor(mongoDatabaseURI = DefaultMongoURI) {
constructor(mongoDatabaseURI = defaults.DefaultMongoURI) {
super();
this._databaseURI = mongoDatabaseURI;
this._connect();

View File

@@ -3,15 +3,13 @@
// Allows you to change the logger mechanism
//
// Adapter classes must implement the following functions:
// * info(obj1 [, obj2, .., objN])
// * error(obj1 [, obj2, .., objN])
// * query(options, callback)
// * log() {}
// * query(options, callback) /* optional */
// Default is WinstonLoggerAdapter.js
export class LoggerAdapter {
info() {}
error() {}
query(options, callback) {}
constructor(options) {}
log(level, message, /* meta */) {}
}
export default LoggerAdapter;

View File

@@ -0,0 +1,100 @@
import winston from 'winston';
import fs from 'fs';
import path from 'path';
import DailyRotateFile from 'winston-daily-rotate-file';
import _ from 'lodash';
import defaults from '../../defaults';
const logger = new winston.Logger();
const additionalTransports = [];
function updateTransports(options) {
let transports = Object.assign({}, logger.transports);
if (options) {
let silent = options.silent;
delete options.silent;
if (_.isNull(options.dirname)) {
delete transports['parse-server'];
delete transports['parse-server-error'];
} else if (!_.isUndefined(options.dirname)) {
transports['parse-server'] = new (DailyRotateFile)(
Object.assign({
filename: 'parse-server.info',
name: 'parse-server',
}, options));
transports['parse-server-error'] = new (DailyRotateFile)(
Object.assign({
filename: 'parse-server.err',
name: 'parse-server-error',
level: 'error'
}, options));
}
transports.console = new (winston.transports.Console)(
Object.assign({
colorize: true,
name: 'console',
silent
}, options));
}
// Mount the additional transports
additionalTransports.forEach((transport) => {
transports[transport.name] = transport;
});
logger.configure({
transports: _.values(transports)
});
}
export function configureLogger({
logsFolder = defaults.logsFolder,
jsonLogs = defaults.jsonLogs,
logLevel = winston.level,
verbose = defaults.verbose,
silent = defaults.silent } = {}) {
if (verbose) {
logLevel = 'verbose';
}
winston.level = logLevel;
const options = {};
if (logsFolder) {
if (!path.isAbsolute(logsFolder)) {
logsFolder = path.resolve(process.cwd(), logsFolder);
}
try {
fs.mkdirSync(logsFolder);
} catch (exception) {}
}
options.dirname = logsFolder;
options.level = logLevel;
options.silent = silent;
if (jsonLogs) {
options.json = true;
options.stringify = true;
}
updateTransports(options);
}
export function addTransport(transport) {
additionalTransports.push(transport);
updateTransports();
}
export function removeTransport(transport) {
let transportName = typeof transport == 'string' ? transport : transport.name;
let transports = Object.assign({}, logger.transports);
delete transports[transportName];
logger.configure({
transports: _.values(transports)
});
_.remove(additionalTransports, (transport) => {
return transport.name === transportName;
});
}
export { logger, addTransport, configureLogger, removeTransport };
export default logger;

View File

@@ -1,63 +1,23 @@
import { LoggerAdapter } from './LoggerAdapter';
import { logger, addTransport } from '../../logger';
import { logger, addTransport, configureLogger } from './WinstonLogger';
const MILLISECONDS_IN_A_DAY = 24 * 60 * 60 * 1000;
const CACHE_TIME = 1000 * 60;
let currentDate = new Date();
let simpleCache = {
timestamp: null,
from: null,
until: null,
order: null,
data: [],
level: 'info',
};
// returns Date object rounded to nearest day
let _getNearestDay = (date) => {
return new Date(date.getFullYear(), date.getMonth(), date.getDate());
}
// returns Date object of previous day
let _getPrevDay = (date) => {
return new Date(date - MILLISECONDS_IN_A_DAY);
}
// returns the iso formatted file name
let _getFileName = () => {
return _getNearestDay(currentDate).toISOString()
}
// check for valid cache when both from and util match.
// cache valid for up to 1 minute
let _hasValidCache = (from, until, level) => {
if (String(from) === String(simpleCache.from) &&
String(until) === String(simpleCache.until) &&
new Date() - simpleCache.timestamp < CACHE_TIME &&
level === simpleCache.level) {
return true;
}
return false;
}
// check that log entry has valid time stamp based on query
let _isValidLogEntry = (from, until, entry) => {
var _entry = JSON.parse(entry),
timestamp = new Date(_entry.timestamp);
return timestamp >= from && timestamp <= until
? true
: false
};
export class WinstonLoggerAdapter extends LoggerAdapter {
info() {
return logger.info.apply(undefined, arguments);
constructor(options) {
super();
if (options) {
configureLogger(options);
}
}
error() {
return logger.error.apply(undefined, arguments);
log() {
return logger.log.apply(logger, arguments);
}
addTransport(transport) {

View File

@@ -17,7 +17,6 @@ let mongodb = require('mongodb');
let MongoClient = mongodb.MongoClient;
const MongoSchemaCollectionName = '_SCHEMA';
const DefaultMongoURI = 'mongodb://localhost:27017/parse';
const storageAdapterAllCollections = mongoAdapter => {
return mongoAdapter.connect()
@@ -86,7 +85,7 @@ export class MongoStorageAdapter {
database;
constructor({
uri = DefaultMongoURI,
uri = defaults.DefaultMongoURI,
collectionPrefix = '',
mongoOptions = {},
}) {

View File

@@ -878,6 +878,8 @@ DatabaseController.prototype.addPointerPermissions = function(schema, className,
}
}
// TODO: create indexes on first creation of a _User object. Otherwise it's impossible to
// have a Parse app without it having a _User collection.
DatabaseController.prototype.performInitizalization = function() {
const requiredUserFields = { fields: { ...SchemaController.defaultColumns._Default, ...SchemaController.defaultColumns._User } };

View File

@@ -16,7 +16,35 @@ export const LogOrder = {
}
export class LoggerController extends AdaptableController {
log(level, args) {
args = [].concat(level, [...args]);
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);
}
// check that date input is valid
static validDateTime(date) {
if (!date) {
@@ -60,6 +88,10 @@ export class LoggerController extends AdaptableController {
throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED,
'Logger adapter is not availabe');
}
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);
}

View File

@@ -3,11 +3,12 @@ const SCHEMA_CACHE_PREFIX = "__SCHEMA";
const ALL_KEYS = "__ALL_KEYS";
import { randomString } from '../cryptoUtils';
import defaults from '../defaults';
export default class SchemaCache {
cache: Object;
constructor(cacheController, ttl = 30) {
constructor(cacheController, ttl = defaults.schemaCacheTTL) {
this.ttl = ttl;
if (typeof ttl == 'string') {
this.ttl = parseInt(ttl);

View File

@@ -1,5 +1,5 @@
import PLog from './PLog';
import Parse from 'parse/node';
import logger from '../logger';
import type { FlattenedObjectData } from './Subscription';
export type Message = { [attr: string]: any };
@@ -37,7 +37,7 @@ class Client {
}
static pushResponse(parseWebSocket: any, message: Message): void {
PLog.verbose('Push Response : %j', message);
logger.verbose('Push Response : %j', message);
parseWebSocket.send(message);
}

View File

@@ -1,5 +0,0 @@
import { addGroup } from '../logger';
let PLog = addGroup('parse-live-query-server');
module.exports = PLog;

View File

@@ -1,5 +1,5 @@
import { ParsePubSub } from './ParsePubSub';
import PLog from './PLog';
import logger from '../logger';
class ParseCloudCodePublisher {
parsePublisher: Object;
@@ -20,7 +20,7 @@ class ParseCloudCodePublisher {
// Request is the request object from cloud code functions. request.object is a ParseObject.
_onCloudCodeMessage(type: string, request: any): void {
PLog.verbose('Raw request from cloud code current : %j | original : %j', request.object, request.original);
logger.verbose('Raw request from cloud code current : %j | original : %j', request.object, request.original);
// We need the full JSON which includes className
let message = {
currentParseObject: request.object._toFullJSON()

View File

@@ -3,7 +3,7 @@ import Parse from 'parse/node';
import { Subscription } from './Subscription';
import { Client } from './Client';
import { ParseWebSocketServer } from './ParseWebSocketServer';
import PLog from './PLog';
import logger from '../logger';
import RequestSchema from './RequestSchema';
import { matchesQuery, queryHash } from './QueryTools';
import { ParsePubSub } from './ParsePubSub';
@@ -25,15 +25,14 @@ class ParseLiveQueryServer {
this.subscriptions = new Map();
config = config || {};
// Set LogLevel
PLog.level = config.logLevel || 'INFO';
// Store keys, convert obj to map
let keyPairs = config.keyPairs || {};
this.keyPairs = new Map();
for (let key of Object.keys(keyPairs)) {
this.keyPairs.set(key, keyPairs[key]);
}
PLog.verbose('Support key pairs', this.keyPairs);
logger.verbose('Support key pairs', this.keyPairs);
// Initialize Parse
Parse.Object.disableSingleInstance();
@@ -62,7 +61,7 @@ class ParseLiveQueryServer {
// Register message handler for subscriber. When publisher get messages, it will publish message
// to the subscribers and the handler will be called.
this.subscriber.on('message', (channel, messageStr) => {
PLog.verbose('Subscribe messsage %j', messageStr);
logger.verbose('Subscribe messsage %j', messageStr);
let message = JSON.parse(messageStr);
this._inflateParseObject(message);
if (channel === 'afterSave') {
@@ -70,7 +69,7 @@ class ParseLiveQueryServer {
} else if (channel === 'afterDelete') {
this._onAfterDelete(message);
} else {
PLog.error('Get message %s from unknown channel %j', message, channel);
logger.error('Get message %s from unknown channel %j', message, channel);
}
});
@@ -100,16 +99,16 @@ class ParseLiveQueryServer {
// Message is the JSON object from publisher after inflated. Message.currentParseObject is the ParseObject after changes.
// Message.originalParseObject is the original ParseObject.
_onAfterDelete(message: any): void {
PLog.verbose('afterDelete is triggered');
logger.verbose('afterDelete is triggered');
let deletedParseObject = message.currentParseObject.toJSON();
let className = deletedParseObject.className;
PLog.verbose('ClassName: %j | ObjectId: %s', className, deletedParseObject.id);
PLog.verbose('Current client number : %d', this.clients.size);
logger.verbose('ClassName: %j | ObjectId: %s', className, deletedParseObject.id);
logger.verbose('Current client number : %d', this.clients.size);
let classSubscriptions = this.subscriptions.get(className);
if (typeof classSubscriptions === 'undefined') {
PLog.error('Can not find subscriptions under this class ' + className);
logger.error('Can not find subscriptions under this class ' + className);
return;
}
for (let subscription of classSubscriptions.values()) {
@@ -131,7 +130,7 @@ class ParseLiveQueryServer {
}
client.pushDelete(requestId, deletedParseObject);
}, (error) => {
PLog.error('Matching ACL error : ', error);
logger.error('Matching ACL error : ', error);
});
}
}
@@ -141,7 +140,7 @@ class ParseLiveQueryServer {
// Message is the JSON object from publisher after inflated. Message.currentParseObject is the ParseObject after changes.
// Message.originalParseObject is the original ParseObject.
_onAfterSave(message: any): void {
PLog.verbose('afterSave is triggered');
logger.verbose('afterSave is triggered');
let originalParseObject = null;
if (message.originalParseObject) {
@@ -149,12 +148,12 @@ class ParseLiveQueryServer {
}
let currentParseObject = message.currentParseObject.toJSON();
let className = currentParseObject.className;
PLog.verbose('ClassName: %s | ObjectId: %s', className, currentParseObject.id);
PLog.verbose('Current client number : %d', this.clients.size);
logger.verbose('ClassName: %s | ObjectId: %s', className, currentParseObject.id);
logger.verbose('Current client number : %d', this.clients.size);
let classSubscriptions = this.subscriptions.get(className);
if (typeof classSubscriptions === 'undefined') {
PLog.error('Can not find subscriptions under this class ' + className);
logger.error('Can not find subscriptions under this class ' + className);
return;
}
for (let subscription of classSubscriptions.values()) {
@@ -192,7 +191,7 @@ class ParseLiveQueryServer {
originalACLCheckingPromise,
currentACLCheckingPromise
).then((isOriginalMatched, isCurrentMatched) => {
PLog.verbose('Original %j | Current %j | Match: %s, %s, %s, %s | Query: %s',
logger.verbose('Original %j | Current %j | Match: %s, %s, %s, %s | Query: %s',
originalParseObject,
currentParseObject,
isOriginalSubscriptionMatched,
@@ -220,7 +219,7 @@ class ParseLiveQueryServer {
let functionName = 'push' + type;
client[functionName](requestId, currentParseObject);
}, (error) => {
PLog.error('Matching ACL error : ', error);
logger.error('Matching ACL error : ', error);
});
}
}
@@ -232,12 +231,12 @@ class ParseLiveQueryServer {
if (typeof request === 'string') {
request = JSON.parse(request);
}
PLog.verbose('Request: %j', request);
logger.verbose('Request: %j', request);
// Check whether this request is a valid request, return error directly if not
if (!tv4.validate(request, RequestSchema['general']) || !tv4.validate(request, RequestSchema[request.op])) {
Client.pushError(parseWebsocket, 1, tv4.error.message);
PLog.error('Connect message error %s', tv4.error.message);
logger.error('Connect message error %s', tv4.error.message);
return;
}
@@ -253,15 +252,15 @@ class ParseLiveQueryServer {
break;
default:
Client.pushError(parseWebsocket, 3, 'Get unknown operation');
PLog.error('Get unknown operation', request.op);
logger.error('Get unknown operation', request.op);
}
});
parseWebsocket.on('disconnect', () => {
PLog.log('Client disconnect: %d', parseWebsocket.clientId);
logger.info('Client disconnect: %d', parseWebsocket.clientId);
let clientId = parseWebsocket.clientId;
if (!this.clients.has(clientId)) {
PLog.error('Can not find client %d on disconnect', clientId);
logger.error('Can not find client %d on disconnect', clientId);
return;
}
@@ -285,8 +284,8 @@ class ParseLiveQueryServer {
}
}
PLog.verbose('Current clients %d', this.clients.size);
PLog.verbose('Current subscriptions %d', this.subscriptions.size);
logger.verbose('Current clients %d', this.clients.size);
logger.verbose('Current subscriptions %d', this.subscriptions.size);
});
}
@@ -331,14 +330,14 @@ class ParseLiveQueryServer {
_handleConnect(parseWebsocket: any, request: any): any {
if (!this._validateKeys(request, this.keyPairs)) {
Client.pushError(parseWebsocket, 4, 'Key in request is not valid');
PLog.error('Key in request is not valid');
logger.error('Key in request is not valid');
return;
}
let client = new Client(this.clientId, parseWebsocket);
parseWebsocket.clientId = this.clientId;
this.clientId += 1;
this.clients.set(parseWebsocket.clientId, client);
PLog.log('Create new client: %d', parseWebsocket.clientId);
logger.info('Create new client: %d', parseWebsocket.clientId);
client.pushConnect();
}
@@ -361,7 +360,7 @@ class ParseLiveQueryServer {
// If we can not find this client, return error to client
if (!parseWebsocket.hasOwnProperty('clientId')) {
Client.pushError(parseWebsocket, 2, 'Can not find this client, make sure you connect to server before subscribing');
PLog.error('Can not find this client, make sure you connect to server before subscribing');
logger.error('Can not find this client, make sure you connect to server before subscribing');
return;
}
let client = this.clients.get(parseWebsocket.clientId);
@@ -400,15 +399,15 @@ class ParseLiveQueryServer {
client.pushSubscribe(request.requestId);
PLog.verbose('Create client %d new subscription: %d', parseWebsocket.clientId, request.requestId);
PLog.verbose('Current client number: %d', this.clients.size);
logger.verbose('Create client %d new subscription: %d', parseWebsocket.clientId, request.requestId);
logger.verbose('Current client number: %d', this.clients.size);
}
_handleUnsubscribe(parseWebsocket: any, request: any): any {
// If we can not find this client, return error to client
if (!parseWebsocket.hasOwnProperty('clientId')) {
Client.pushError(parseWebsocket, 2, 'Can not find this client, make sure you connect to server before unsubscribing');
PLog.error('Can not find this client, make sure you connect to server before unsubscribing');
logger.error('Can not find this client, make sure you connect to server before unsubscribing');
return;
}
let requestId = request.requestId;
@@ -416,7 +415,7 @@ class ParseLiveQueryServer {
if (typeof client === 'undefined') {
Client.pushError(parseWebsocket, 2, 'Cannot find client with clientId ' + parseWebsocket.clientId +
'. Make sure you connect to live query server before unsubscribing.');
PLog.error('Can not find this client ' + parseWebsocket.clientId);
logger.error('Can not find this client ' + parseWebsocket.clientId);
return;
}
@@ -424,7 +423,7 @@ class ParseLiveQueryServer {
if (typeof subscriptionInfo === 'undefined') {
Client.pushError(parseWebsocket, 2, 'Cannot find subscription with clientId ' + parseWebsocket.clientId +
' subscriptionId ' + requestId + '. Make sure you subscribe to live query server before unsubscribing.');
PLog.error('Can not find subscription with clientId ' + parseWebsocket.clientId + ' subscriptionId ' + requestId);
logger.error('Can not find subscription with clientId ' + parseWebsocket.clientId + ' subscriptionId ' + requestId);
return;
}
@@ -446,14 +445,10 @@ class ParseLiveQueryServer {
client.pushUnsubscribe(request.requestId);
PLog.verbose('Delete client: %d | subscription: %d', parseWebsocket.clientId, request.requestId);
logger.verbose('Delete client: %d | subscription: %d', parseWebsocket.clientId, request.requestId);
}
}
ParseLiveQueryServer.setLogLevel = function(logLevel) {
PLog.logLevel = logLevel;
}
export {
ParseLiveQueryServer
}

View File

@@ -1,4 +1,4 @@
import PLog from './PLog';
import logger from '../logger';
let typeMap = new Map([['disconnect', 'close']]);
@@ -9,7 +9,7 @@ export class ParseWebSocketServer {
let WebSocketServer = require('ws').Server;
let wss = new WebSocketServer({ server: server });
wss.on('listening', () => {
PLog.log('Parse LiveQuery Server starts running');
logger.info('Parse LiveQuery Server starts running');
});
wss.on('connection', (ws) => {
onConnect(new ParseWebSocket(ws));

View File

@@ -1,6 +1,6 @@
import Parse from 'parse/node';
import LRU from 'lru-cache';
import PLog from './PLog';
import logger from '../logger';
class SessionTokenCache {
cache: Object;
@@ -18,16 +18,16 @@ class SessionTokenCache {
}
let userId = this.cache.get(sessionToken);
if (userId) {
PLog.verbose('Fetch userId %s of sessionToken %s from Cache', userId, sessionToken);
logger.verbose('Fetch userId %s of sessionToken %s from Cache', userId, sessionToken);
return Parse.Promise.as(userId);
}
return Parse.User.become(sessionToken).then((user) => {
PLog.verbose('Fetch userId %s of sessionToken %s from Parse', user.id, sessionToken);
logger.verbose('Fetch userId %s of sessionToken %s from Parse', user.id, sessionToken);
let userId = user.id;
this.cache.set(sessionToken, userId);
return Parse.Promise.as(userId);
}, (error) => {
PLog.error('Can not fetch userId for sessionToken %j, error %j', sessionToken, error);
logger.error('Can not fetch userId for sessionToken %j, error %j', sessionToken, error);
return Parse.Promise.error(error);
});
}

View File

@@ -1,5 +1,5 @@
import {matchesQuery, queryHash} from './QueryTools';
import PLog from './PLog';
import {matchesQuery, queryHash} from './QueryTools';
import logger from '../logger';
export type FlattenedObjectData = { [attr: string]: any };
export type QueryData = { [attr: string]: any };
@@ -29,13 +29,13 @@ class Subscription {
deleteClientSubscription(clientId: number, requestId: number): void {
let requestIds = this.clientRequestIds.get(clientId);
if (typeof requestIds === 'undefined') {
PLog.error('Can not find client %d to delete', clientId);
logger.error('Can not find client %d to delete', clientId);
return;
}
let index = requestIds.indexOf(requestId);
if (index < 0) {
PLog.error('Can not find client %d subscription %d to delete', clientId, requestId);
logger.error('Can not find client %d subscription %d to delete', clientId, requestId);
return;
}
requestIds.splice(index, 1);

View File

@@ -13,8 +13,8 @@ if (!global._babelPolyfill) {
require('babel-polyfill');
}
import { logger,
configureLogger } from './logger';
import defaults from './defaults';
import * as logging from './logger';
import AppCache from './cache';
import Config from './Config';
import parseServerPackage from '../package.json';
@@ -94,13 +94,16 @@ class ParseServer {
appId = requiredParameter('You must provide an appId!'),
masterKey = requiredParameter('You must provide a masterKey!'),
appName,
analyticsAdapter = undefined,
analyticsAdapter,
filesAdapter,
push,
loggerAdapter,
jsonLogs,
logsFolder,
databaseURI,
jsonLogs = defaults.jsonLogs,
logsFolder = defaults.logsFolder,
verbose = defaults.verbose,
logLevel = defaults.level,
silent = defaults.silent,
databaseURI = defaults.DefaultMongoURI,
databaseOptions,
databaseAdapter,
cloud,
@@ -110,15 +113,15 @@ class ParseServer {
dotNetKey,
restAPIKey,
webhookKey,
fileKey = undefined,
fileKey,
facebookAppIds = [],
enableAnonymousUsers = true,
allowClientClassCreation = true,
enableAnonymousUsers = defaults.enableAnonymousUsers,
allowClientClassCreation = defaults.allowClientClassCreation,
oauth = {},
serverURL = requiredParameter('You must provide a serverURL!'),
maxUploadSize = '20mb',
verifyUserEmails = false,
preventLoginWithUnverifiedEmail = false,
maxUploadSize = defaults.maxUploadSize,
verifyUserEmails = defaults.verifyUserEmails,
preventLoginWithUnverifiedEmail = defaults.preventLoginWithUnverifiedEmail,
emailVerifyTokenValidityDuration,
cacheAdapter,
emailAdapter,
@@ -130,17 +133,16 @@ class ParseServer {
passwordResetSuccess: undefined
},
liveQuery = {},
sessionLength = 31536000, // 1 Year in seconds
expireInactiveSessions = true,
verbose = false,
revokeSessionOnPasswordReset = true,
schemaCacheTTL = 5, // cache for 5s
sessionLength = defaults.sessionLength, // 1 Year in seconds
expireInactiveSessions = defaults.expireInactiveSessions,
revokeSessionOnPasswordReset = defaults.revokeSessionOnPasswordReset,
schemaCacheTTL = defaults.schemaCacheTTL, // cache for 5s
__indexBuildCompletionCallbackForTests = () => {},
}) {
// Initialize the node client SDK automatically
Parse.initialize(appId, javascriptKey || 'unused', masterKey);
Parse.serverURL = serverURL;
if ((databaseOptions || databaseURI || collectionPrefix !== '') && databaseAdapter) {
if ((databaseOptions || (databaseURI && databaseURI != defaults.DefaultMongoURI) || collectionPrefix !== '') && databaseAdapter) {
throw 'You cannot specify both a databaseAdapter and a databaseURI/databaseOptions/connectionPrefix.';
} else if (!databaseAdapter) {
databaseAdapter = new MongoStorageAdapter({
@@ -156,49 +158,34 @@ class ParseServer {
throw 'When using an explicit database adapter, you must also use and explicit filesAdapter.';
}
if (logsFolder) {
configureLogger({logsFolder, jsonLogs});
}
if (cloud) {
addParseCloud();
if (typeof cloud === 'function') {
cloud(Parse)
} else if (typeof cloud === 'string') {
require(path.resolve(process.cwd(), cloud));
} else {
throw "argument 'cloud' must either be a string or a function";
}
}
if (verbose || process.env.VERBOSE || process.env.VERBOSE_PARSE_SERVER) {
configureLogger({level: 'silly', jsonLogs});
}
const loggerControllerAdapter = loadAdapter(loggerAdapter, WinstonLoggerAdapter, { jsonLogs, logsFolder, verbose, logLevel, silent });
const loggerController = new LoggerController(loggerControllerAdapter, appId);
logging.setLogger(loggerController);
const filesControllerAdapter = loadAdapter(filesAdapter, () => {
return new GridStoreAdapter(databaseURI);
});
const filesController = new FilesController(filesControllerAdapter, appId);
// Pass the push options too as it works with the default
const pushControllerAdapter = loadAdapter(push && push.adapter, ParsePushAdapter, push || {});
const loggerControllerAdapter = loadAdapter(loggerAdapter, WinstonLoggerAdapter);
const emailControllerAdapter = loadAdapter(emailAdapter);
const cacheControllerAdapter = loadAdapter(cacheAdapter, InMemoryCacheAdapter, {appId: appId});
const analyticsControllerAdapter = loadAdapter(analyticsAdapter, AnalyticsAdapter);
// We pass the options and the base class for the adatper,
// Note that passing an instance would work too
const filesController = new FilesController(filesControllerAdapter, appId);
const pushController = new PushController(pushControllerAdapter, appId, push);
const loggerController = new LoggerController(loggerControllerAdapter, appId);
const emailControllerAdapter = loadAdapter(emailAdapter);
const userController = new UserController(emailControllerAdapter, appId, { verifyUserEmails });
const liveQueryController = new LiveQueryController(liveQuery);
const cacheControllerAdapter = loadAdapter(cacheAdapter, InMemoryCacheAdapter, {appId: appId});
const cacheController = new CacheController(cacheControllerAdapter, appId);
const databaseController = new DatabaseController(databaseAdapter, new SchemaCache(cacheController, schemaCacheTTL));
const hooksController = new HooksController(appId, databaseController, webhookKey);
const analyticsControllerAdapter = loadAdapter(analyticsAdapter, AnalyticsAdapter);
const analyticsController = new AnalyticsController(analyticsControllerAdapter);
// TODO: create indexes on first creation of a _User object. Otherwise it's impossible to
// have a Parse app without it having a _User collection.
const liveQueryController = new LiveQueryController(liveQuery);
const databaseController = new DatabaseController(databaseAdapter, new SchemaCache(cacheController, schemaCacheTTL));
const hooksController = new HooksController(appId, databaseController, webhookKey);
const dbInitPromise = databaseController.performInitizalization();
AppCache.put(appId, {
@@ -251,6 +238,17 @@ class ParseServer {
if (process.env.TESTING) {
__indexBuildCompletionCallbackForTests(dbInitPromise);
}
if (cloud) {
addParseCloud();
if (typeof cloud === 'function') {
cloud(Parse)
} else if (typeof cloud === 'string') {
require(path.resolve(process.cwd(), cloud));
} else {
throw "argument 'cloud' must either be a string or a function";
}
}
}
get app() {

View File

@@ -65,7 +65,7 @@ export class FunctionsRouter extends PromiseRouter {
master: req.auth && req.auth.isMaster,
user: req.auth && req.auth.user,
installationId: req.info.installationId,
log: req.config.loggerController && req.config.loggerController.adapter,
log: req.config.loggerController,
headers: req.headers,
functionName: req.params.functionName
};

View File

@@ -32,6 +32,13 @@ function booleanParser(opt) {
return false;
}
function nullParser(opt) {
if (opt == 'null') {
return null;
}
return opt;
}
export default {
"appId": {
env: "PARSE_SERVER_APPLICATION_ID",
@@ -193,6 +200,18 @@ export default {
env: "JSON_LOGS",
help: "Log as structured JSON objects"
},
"logLevel": {
env: "PARSE_SERVER_LOG_LEVEL",
help: "Sets the level for logs"
},
"logsFolder": {
env: "PARSE_SERVER_LOGS_FOLDER",
help: "Folder for the logs (defaults to './logs'); set to null to disable file based logging",
action: nullParser
},
"silent": {
help: "Disables console output",
},
"revokeSessionOnPasswordReset": {
env: "PARSE_SERVER_REVOKE_SESSION_ON_PASSWORD_RESET",
help: "When a user changes their password, either through the reset password email or while logged in, all sessions are revoked if this is true. Set to false if you don't want to revoke sessions.",

31
src/defaults.js Normal file
View File

@@ -0,0 +1,31 @@
let logsFolder = (() => {
let folder = './logs/';
if (typeof process !== 'undefined' && process.env.NODE_ENV === 'test') {
folder = './test_logs/'
}
folder = process.env.PARSE_SERVER_LOGS_FOLDER || folder;
return folder;
})();
let { verbose, level } = (() => {
let verbose = process.env.VERBOSE ? true : false;
return { verbose, level: verbose ? 'verbose' : undefined }
})();
export default {
DefaultMongoURI: 'mongodb://localhost:27017/parse',
jsonLogs: process.env.JSON_LOGS || false,
logsFolder,
verbose,
level,
silent: false,
enableAnonymousUsers: true,
allowClientClassCreation: true,
maxUploadSize: '20mb',
verifyUserEmails: false,
preventLoginWithUnverifiedEmail: false,
sessionLength: 31536000,
expireInactiveSessions: true,
revokeSessionOnPasswordReset: true,
schemaCacheTTL: 5000 // in ms
}

View File

@@ -1,10 +1,10 @@
import ParseServer from './ParseServer';
import logger from './logger';
import S3Adapter from 'parse-server-s3-adapter'
import FileSystemAdapter from 'parse-server-fs-adapter'
import InMemoryCacheAdapter from './Adapters/Cache/InMemoryCacheAdapter'
import TestUtils from './TestUtils';
import { useExternal } from './deprecated'
import { useExternal } from './deprecated';
import { getLogger } from './logger';
// Factory function
let _ParseServer = function(options) {
@@ -16,5 +16,9 @@ _ParseServer.createLiveQueryServer = ParseServer.createLiveQueryServer;
let GCSAdapter = useExternal('GCSAdapter', 'parse-server-gcs-adapter');
Object.defineProperty(module.exports, 'logger', {
get: getLogger
});
export default ParseServer;
export { S3Adapter, GCSAdapter, FileSystemAdapter, InMemoryCacheAdapter, TestUtils, logger, _ParseServer as ParseServer };
export { S3Adapter, GCSAdapter, FileSystemAdapter, InMemoryCacheAdapter, TestUtils, _ParseServer as ParseServer };

View File

@@ -1,104 +1,20 @@
import winston from 'winston';
import fs from 'fs';
import path from 'path';
import DailyRotateFile from 'winston-daily-rotate-file';
'use strict';
let logger;
let LOGS_FOLDER = './logs/';
if (typeof process !== 'undefined' && process.env.NODE_ENV === 'test') {
LOGS_FOLDER = './test_logs/'
export function setLogger(aLogger) {
logger = aLogger;
}
LOGS_FOLDER = process.env.PARSE_SERVER_LOGS_FOLDER || LOGS_FOLDER;
const JSON_LOGS = process.env.JSON_LOGS || false;
let currentLogsFolder = LOGS_FOLDER;
const additionalTransports = [];
function generateTransports(level, options = {}) {
let transports = [
new (DailyRotateFile)(
Object.assign({
filename: 'parse-server.info',
dirname: currentLogsFolder,
name: 'parse-server',
level: level
}, options)
),
new (DailyRotateFile)(
Object.assign({
filename: 'parse-server.err',
dirname: currentLogsFolder,
name: 'parse-server-error',
level: 'error'
}
), options)
].concat(additionalTransports);
if (!process.env.TESTING || process.env.VERBOSE) {
transports = [
new (winston.transports.Console)(
Object.assign({
colorize: true,
level: level
}, options)
)
].concat(transports);
}
return transports;
export function getLogger() {
return logger;
}
const logger = new winston.Logger();
// for: `import logger from './logger'`
Object.defineProperty(module.exports, 'default', {
get: getLogger
});
export function configureLogger({ logsFolder, jsonLogs, level = winston.level }) {
winston.level = level;
logsFolder = logsFolder || currentLogsFolder;
if (!path.isAbsolute(logsFolder)) {
logsFolder = path.resolve(process.cwd(), logsFolder);
}
try {
fs.mkdirSync(logsFolder);
} catch (exception) {
// Ignore, assume the folder already exists
}
currentLogsFolder = logsFolder;
const options = {};
if (jsonLogs) {
options.json = true;
options.stringify = true;
}
const transports = generateTransports(level, options);
logger.configure({
transports: transports
})
}
configureLogger({ logsFolder: LOGS_FOLDER, jsonLogs: JSON_LOGS });
export function addGroup(groupName) {
let level = winston.level;
let transports = generateTransports().concat(new (DailyRotateFile)({
filename: groupName,
dirname: currentLogsFolder,
name: groupName,
level: level
}));
winston.loggers.add(groupName, {
transports: transports
});
return winston.loggers.get(groupName);
}
export function addTransport(transport) {
const level = winston.level;
additionalTransports.push(transport);
const transports = generateTransports(level);
logger.configure({
transports: transports
});
}
export { logger, addTransport };
export default logger;
// for: `import { logger } from './logger'`
Object.defineProperty(module.exports, 'logger', {
get: getLogger
});

View File

@@ -102,7 +102,7 @@ export function getRequestObject(triggerType, auth, parseObject, originalParseOb
triggerName: triggerType,
object: parseObject,
master: false,
log: config.loggerController && config.loggerController.adapter
log: config.loggerController
};
if (originalParseObject) {