diff --git a/spec/AdapterLoader.spec.js b/spec/AdapterLoader.spec.js index 250a8a7c49..8c2e1aef96 100644 --- a/spec/AdapterLoader.spec.js +++ b/spec/AdapterLoader.spec.js @@ -45,6 +45,19 @@ describe("AdapterLoader", ()=>{ done(); }); + it("should instantiate an adapter from npm module", (done) => { + var adapter = loadAdapter({ + module: 'parse-server-fs-adapter' + }); + + expect(typeof adapter).toBe('object'); + expect(typeof adapter.createFile).toBe('function'); + expect(typeof adapter.deleteFile).toBe('function'); + expect(typeof adapter.getFileData).toBe('function'); + expect(typeof adapter.getFileLocation).toBe('function'); + done(); + }); + it("should instantiate an adapter from function/Class", (done) => { var adapter = loadAdapter({ adapter: FilesAdapter diff --git a/spec/InstallationsRouter.spec.js b/spec/InstallationsRouter.spec.js index 60965ff967..b2725a09af 100644 --- a/spec/InstallationsRouter.spec.js +++ b/spec/InstallationsRouter.spec.js @@ -5,7 +5,7 @@ var InstallationsRouter = require('../src/Routers/InstallationsRouter').Installa var config = new Config('test'); -describe('InstallationsRouter', () => { +describe_only_db(['mongo'])('InstallationsRouter', () => { it('uses find condition from request.body', (done) => { var androidDeviceRequest = { 'installationId': '12345678-abcd-abcd-abcd-123456789abc', @@ -71,6 +71,9 @@ describe('InstallationsRouter', () => { var results = res.response.results; expect(results.length).toEqual(1); done(); + }).catch((err) => { + fail(JSON.stringify(err)); + done(); }); }); @@ -172,6 +175,9 @@ describe('InstallationsRouter', () => { expect(response.results.length).toEqual(0); expect(response.count).toEqual(2); done(); + }).catch((err) => { + fail(JSON.stringify(err)); + done(); }); }); }); diff --git a/spec/Logger.spec.js b/spec/Logger.spec.js index 37f8f871af..a131e0d53c 100644 --- a/spec/Logger.spec.js +++ b/spec/Logger.spec.js @@ -1,4 +1,4 @@ -var logger = require('../src/logger'); +var logging = require('../src/Adapters/Logger/WinstonLogger'); var winston = require('winston'); class TestTransport extends winston.Transport { @@ -9,10 +9,55 @@ class TestTransport extends winston.Transport { describe('Logger', () => { it('should add transport', () => { - const testTransport = new (TestTransport)({}); + const testTransport = new (TestTransport)({ + name: 'test' + }); spyOn(testTransport, 'log'); - logger.addTransport(testTransport); - logger.logger.info('hi'); + logging.addTransport(testTransport); + expect(Object.keys(logging.logger.transports).length).toBe(4); + logging.logger.info('hi'); expect(testTransport.log).toHaveBeenCalled(); + logging.removeTransport(testTransport); + expect(Object.keys(logging.logger.transports).length).toBe(3); + }); + + it('should have files transports', (done) => { + reconfigureServer().then(() => { + let transports = logging.logger.transports; + let transportKeys = Object.keys(transports); + expect(transportKeys.length).toBe(3); + done(); + }); + }); + + it('should disable files logs', (done) => { + reconfigureServer({ + logsFolder: null + }).then(() => { + let transports = logging.logger.transports; + let transportKeys = Object.keys(transports); + expect(transportKeys.length).toBe(1); + done(); + }); + }); + + it('should enable JSON logs', (done) => { + // Force console transport + reconfigureServer({ + logsFolder: null, + jsonLogs: true, + silent: false + }).then(() => { + let spy = spyOn(process.stdout, 'write'); + logging.logger.info('hi', {key: 'value'}); + expect(process.stdout.write).toHaveBeenCalled(); + var firstLog = process.stdout.write.calls.first().args[0]; + expect(firstLog).toEqual(JSON.stringify({key: 'value', level: 'info', message: 'hi' })+'\n'); + return reconfigureServer({ + jsonLogs: false + }); + }).then(() => { + done(); + }); }); }); diff --git a/spec/Subscription.spec.js b/spec/Subscription.spec.js index a9f35020be..20f1aa5bc1 100644 --- a/spec/Subscription.spec.js +++ b/spec/Subscription.spec.js @@ -1,10 +1,10 @@ var Subscription = require('../src/LiveQuery/Subscription').Subscription; - +let logger; describe('Subscription', function() { beforeEach(function() { - var mockError = jasmine.createSpy('error'); - jasmine.mockLibrary('../src/LiveQuery/PLog', 'error', mockError); + logger = require('../src/logger').logger; + spyOn(logger, 'error').and.callThrough(); }); it('can be initialized', function() { @@ -62,8 +62,7 @@ describe('Subscription', function() { var subscription = new Subscription('className', { key : 'value' }, 'hash'); subscription.deleteClientSubscription(1, 1); - var PLog =require('../src/LiveQuery/PLog'); - expect(PLog.error).toHaveBeenCalled(); + expect(logger.error).toHaveBeenCalled(); }); it('can delete nonexistent request for one client', function() { @@ -71,8 +70,7 @@ describe('Subscription', function() { subscription.addClientSubscription(1, 1); subscription.deleteClientSubscription(1, 2); - var PLog =require('../src/LiveQuery/PLog'); - expect(PLog.error).toHaveBeenCalled(); + expect(logger.error).toHaveBeenCalled(); expect(subscription.clientRequestIds.size).toBe(1); expect(subscription.clientRequestIds.get(1)).toEqual([1]); }); @@ -83,8 +81,7 @@ describe('Subscription', function() { subscription.addClientSubscription(1, 2); subscription.deleteClientSubscription(1, 2); - var PLog =require('../src/LiveQuery/PLog'); - expect(PLog.error).not.toHaveBeenCalled(); + expect(logger.error).not.toHaveBeenCalled(); expect(subscription.clientRequestIds.size).toBe(1); expect(subscription.clientRequestIds.get(1)).toEqual([1]); }); @@ -96,8 +93,7 @@ describe('Subscription', function() { subscription.deleteClientSubscription(1, 1); subscription.deleteClientSubscription(1, 2); - var PLog =require('../src/LiveQuery/PLog'); - expect(PLog.error).not.toHaveBeenCalled(); + expect(logger.error).not.toHaveBeenCalled(); expect(subscription.clientRequestIds.size).toBe(0); }); @@ -111,13 +107,8 @@ describe('Subscription', function() { subscription.deleteClientSubscription(2, 1); subscription.deleteClientSubscription(2, 2); - var PLog =require('../src/LiveQuery/PLog'); - expect(PLog.error).not.toHaveBeenCalled(); + expect(logger.error).not.toHaveBeenCalled(); expect(subscription.clientRequestIds.size).toBe(1); expect(subscription.clientRequestIds.get(1)).toEqual([1]); }); - - afterEach(function(){ - jasmine.restoreLibrary('../src/LiveQuery/PLog', 'error'); - }); }); diff --git a/spec/WinstonLoggerAdapter.spec.js b/spec/WinstonLoggerAdapter.spec.js index ee2a72d40b..fa813756f1 100644 --- a/spec/WinstonLoggerAdapter.spec.js +++ b/spec/WinstonLoggerAdapter.spec.js @@ -8,7 +8,7 @@ describe('info logs', () => { it("Verify INFO logs", (done) => { var winstonLoggerAdapter = new WinstonLoggerAdapter(); - winstonLoggerAdapter.info('testing info logs', () => { + winstonLoggerAdapter.log('info', 'testing info logs', () => { winstonLoggerAdapter.query({ from: new Date(Date.now() - 500), size: 100, @@ -29,7 +29,7 @@ describe('info logs', () => { describe('error logs', () => { it("Verify ERROR logs", (done) => { var winstonLoggerAdapter = new WinstonLoggerAdapter(); - winstonLoggerAdapter.error('testing error logs', () => { + winstonLoggerAdapter.log('error', 'testing error logs', () => { winstonLoggerAdapter.query({ from: new Date(Date.now() - 500), size: 100, diff --git a/spec/helper.js b/spec/helper.js index e9095d6188..37d169f930 100644 --- a/spec/helper.js +++ b/spec/helper.js @@ -45,6 +45,7 @@ var defaultConfiguration = { webhookKey: 'hook', masterKey: 'test', fileKey: 'test', + silent: !process.env.VERBOSE, push: { 'ios': { cert: 'prodCert.pem', @@ -352,8 +353,6 @@ global.describe_only_db = db => { } } -// LiveQuery test setting -require('../src/LiveQuery/PLog').logLevel = 'NONE'; var libraryCache = {}; jasmine.mockLibrary = function(library, name, mock) { var original = require(library)[name]; diff --git a/src/Adapters/Files/GridStoreAdapter.js b/src/Adapters/Files/GridStoreAdapter.js index d7844a0b5f..a2584dc442 100644 --- a/src/Adapters/Files/GridStoreAdapter.js +++ b/src/Adapters/Files/GridStoreAdapter.js @@ -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; - constructor(mongoDatabaseURI = DefaultMongoURI) { + constructor(mongoDatabaseURI = defaults.DefaultMongoURI) { super(); this._databaseURI = mongoDatabaseURI; this._connect(); diff --git a/src/Adapters/Logger/LoggerAdapter.js b/src/Adapters/Logger/LoggerAdapter.js index 9fda8eab0d..4a06558941 100644 --- a/src/Adapters/Logger/LoggerAdapter.js +++ b/src/Adapters/Logger/LoggerAdapter.js @@ -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; diff --git a/src/Adapters/Logger/WinstonLogger.js b/src/Adapters/Logger/WinstonLogger.js new file mode 100644 index 0000000000..2015908b30 --- /dev/null +++ b/src/Adapters/Logger/WinstonLogger.js @@ -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; diff --git a/src/Adapters/Logger/WinstonLoggerAdapter.js b/src/Adapters/Logger/WinstonLoggerAdapter.js index d3728f2844..c13975944f 100644 --- a/src/Adapters/Logger/WinstonLoggerAdapter.js +++ b/src/Adapters/Logger/WinstonLoggerAdapter.js @@ -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) { diff --git a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js index 4bdb575b12..44c116ad7f 100644 --- a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js +++ b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js @@ -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 = {}, }) { diff --git a/src/Controllers/DatabaseController.js b/src/Controllers/DatabaseController.js index 9f43943325..f0dbb902cd 100644 --- a/src/Controllers/DatabaseController.js +++ b/src/Controllers/DatabaseController.js @@ -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 } }; diff --git a/src/Controllers/LoggerController.js b/src/Controllers/LoggerController.js index 7cfbe41ec0..5b1946e9e6 100644 --- a/src/Controllers/LoggerController.js +++ b/src/Controllers/LoggerController.js @@ -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); } diff --git a/src/Controllers/SchemaCache.js b/src/Controllers/SchemaCache.js index 7a56f10710..a72fc0210d 100644 --- a/src/Controllers/SchemaCache.js +++ b/src/Controllers/SchemaCache.js @@ -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); diff --git a/src/LiveQuery/Client.js b/src/LiveQuery/Client.js index 72e4a9d393..8d84429010 100644 --- a/src/LiveQuery/Client.js +++ b/src/LiveQuery/Client.js @@ -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); } diff --git a/src/LiveQuery/PLog.js b/src/LiveQuery/PLog.js deleted file mode 100644 index 8ae8f69145..0000000000 --- a/src/LiveQuery/PLog.js +++ /dev/null @@ -1,5 +0,0 @@ -import { addGroup } from '../logger'; - -let PLog = addGroup('parse-live-query-server'); - -module.exports = PLog; diff --git a/src/LiveQuery/ParseCloudCodePublisher.js b/src/LiveQuery/ParseCloudCodePublisher.js index ac5e9d3483..b50a50820d 100644 --- a/src/LiveQuery/ParseCloudCodePublisher.js +++ b/src/LiveQuery/ParseCloudCodePublisher.js @@ -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() diff --git a/src/LiveQuery/ParseLiveQueryServer.js b/src/LiveQuery/ParseLiveQueryServer.js index 0d59211f4b..044c653660 100644 --- a/src/LiveQuery/ParseLiveQueryServer.js +++ b/src/LiveQuery/ParseLiveQueryServer.js @@ -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 } diff --git a/src/LiveQuery/ParseWebSocketServer.js b/src/LiveQuery/ParseWebSocketServer.js index e97223063b..b7c331c493 100644 --- a/src/LiveQuery/ParseWebSocketServer.js +++ b/src/LiveQuery/ParseWebSocketServer.js @@ -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)); diff --git a/src/LiveQuery/SessionTokenCache.js b/src/LiveQuery/SessionTokenCache.js index 07d9d62744..57f84720d0 100644 --- a/src/LiveQuery/SessionTokenCache.js +++ b/src/LiveQuery/SessionTokenCache.js @@ -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); }); } diff --git a/src/LiveQuery/Subscription.js b/src/LiveQuery/Subscription.js index e3b63dafd3..56cf27b243 100644 --- a/src/LiveQuery/Subscription.js +++ b/src/LiveQuery/Subscription.js @@ -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); diff --git a/src/ParseServer.js b/src/ParseServer.js index 243908db48..c6290f690e 100644 --- a/src/ParseServer.js +++ b/src/ParseServer.js @@ -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 analyticsControllerAdapter = loadAdapter(analyticsAdapter, AnalyticsAdapter); + const analyticsController = new AnalyticsController(analyticsControllerAdapter); + + const liveQueryController = new LiveQueryController(liveQuery); const databaseController = new DatabaseController(databaseAdapter, new SchemaCache(cacheController, schemaCacheTTL)); const hooksController = new HooksController(appId, databaseController, webhookKey); - 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 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() { diff --git a/src/Routers/FunctionsRouter.js b/src/Routers/FunctionsRouter.js index 78635c9326..c61c59fb7f 100644 --- a/src/Routers/FunctionsRouter.js +++ b/src/Routers/FunctionsRouter.js @@ -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 }; diff --git a/src/cli/cli-definitions.js b/src/cli/cli-definitions.js index 9f1d7565bc..b8acc3de26 100644 --- a/src/cli/cli-definitions.js +++ b/src/cli/cli-definitions.js @@ -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.", diff --git a/src/defaults.js b/src/defaults.js new file mode 100644 index 0000000000..838346d623 --- /dev/null +++ b/src/defaults.js @@ -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 +} diff --git a/src/index.js b/src/index.js index 82d3c35a19..8f3e0e2d04 100644 --- a/src/index.js +++ b/src/index.js @@ -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 }; diff --git a/src/logger.js b/src/logger.js index 15ae1f6b74..7c82574333 100644 --- a/src/logger.js +++ b/src/logger.js @@ -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(); - -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 - }); -} +// for: `import logger from './logger'` +Object.defineProperty(module.exports, 'default', { + get: getLogger +}); -export { logger, addTransport }; -export default logger; +// for: `import { logger } from './logger'` +Object.defineProperty(module.exports, 'logger', { + get: getLogger +}); diff --git a/src/triggers.js b/src/triggers.js index ea1853f99e..da74b4fc27 100644 --- a/src/triggers.js +++ b/src/triggers.js @@ -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) {