diff --git a/.eslintrc b/.eslintrc
index 1128872ff..d712529ce 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -17,7 +17,6 @@
"object-curly-spacing": "off",
"max-len": "off",
"new-cap": ["error", {"capIsNew": false}],
- "no-console": "off",
"no-constant-condition": "off",
"no-loop-func": "off",
"no-shadow": "error",
diff --git a/.gitignore b/.gitignore
index ce1018e2e..e5ddc09e2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,6 +4,7 @@ dump.rdb
*npm-debug.log
storage/
lerna-debug.log
+newrelic_agent.log
# Elastic Beanstalk Files
.elasticbeanstalk/*
diff --git a/README.md b/README.md
index 83e953c62..76f21b673 100644
--- a/README.md
+++ b/README.md
@@ -28,7 +28,9 @@
# Developing Locally:
```
-npm start
+npm run start
+npm run logs
+npm run stop
```
We use [pm2](http://pm2.keymetrics.io/) to launch a variety of processes
diff --git a/package.json b/package.json
index c73ac7f1a..625d7d2a7 100644
--- a/package.json
+++ b/package.json
@@ -5,6 +5,8 @@
"main": "",
"dependencies": {
"bluebird": "3.x.x",
+ "bunyan": "1.8.0",
+ "bunyan-cloudwatch": "2.0.0",
"lerna": "2.0.0-beta.23",
"mysql": "^2.11.1",
"newrelic": "^1.28.1",
@@ -16,6 +18,7 @@
},
"devDependencies": {
"babel-eslint": "6.x",
+ "bunyan-prettystream": "^0.1.3",
"eslint": "2.x",
"eslint-config-airbnb": "8.x",
"eslint-plugin-import": "1.x",
@@ -25,8 +28,9 @@
"sqlite3": "https://github.com/bengotow/node-sqlite3/archive/bengotow/usleep-v3.1.4.tar.gz"
},
"scripts": {
- "start": "./node_modules/pm2/bin/pm2 start ./pm2-dev.yml --no-daemon",
- "postinstall": "node_modules/.bin/lerna bootstrap"
+ "start": "pm2 start ./pm2-dev.yml --no-daemon",
+ "stop": "pm2 kill",
+ "postinstall": "lerna bootstrap"
},
"repository": {
"type": "git",
diff --git a/packages/nylas-api/app.js b/packages/nylas-api/app.js
index bca30c9af..dc9b08e08 100644
--- a/packages/nylas-api/app.js
+++ b/packages/nylas-api/app.js
@@ -1,3 +1,5 @@
+// require('newrelic');
+
const Hapi = require('hapi');
const HapiSwagger = require('hapi-swagger');
const HapiBoom = require('hapi-boom-decorators')
@@ -7,8 +9,10 @@ const Vision = require('vision');
const Package = require('./package');
const fs = require('fs');
const path = require('path');
+const {DatabaseConnector, SchedulerUtils, Logger} = require(`nylas-core`);
global.Promise = require('bluebird');
+global.Logger = Logger.createLogger('nylas-k2-api')
const server = new Hapi.Server({
connections: {
@@ -33,8 +37,6 @@ const plugins = [Inert, Vision, HapiBasicAuth, HapiBoom, {
let sharedDb = null;
const validate = (request, username, password, callback) => {
- const {DatabaseConnector, SchedulerUtils} = require(`nylas-core`);
-
let getSharedDb = null;
if (sharedDb) {
getSharedDb = Promise.resolve(sharedDb)
@@ -88,6 +90,6 @@ server.register(plugins, (err) => {
server.start((startErr) => {
if (startErr) { throw startErr; }
- console.log('API running at:', server.info.uri);
+ global.Logger.info({url: server.info.uri}, 'API running');
});
});
diff --git a/packages/nylas-api/decorators/connections.js b/packages/nylas-api/decorators/connections.js
index 2003696cf..161e9beb6 100644
--- a/packages/nylas-api/decorators/connections.js
+++ b/packages/nylas-api/decorators/connections.js
@@ -7,4 +7,10 @@ module.exports = (server) => {
const account = this.auth.credentials;
return DatabaseConnector.forAccount(account.id);
});
+ server.decorate('request', 'logger', (request) => {
+ if (request.auth.credentials) {
+ return global.Logger.forAccount(request.auth.credentials)
+ }
+ return global.Logger
+ }, {apply: true});
}
diff --git a/packages/nylas-api/newrelic.js b/packages/nylas-api/newrelic.js
new file mode 100644
index 000000000..00882097e
--- /dev/null
+++ b/packages/nylas-api/newrelic.js
@@ -0,0 +1,24 @@
+/**
+ * New Relic agent configuration.
+ *
+ * See lib/config.defaults.js in the agent distribution for a more complete
+ * description of configuration variables and their potential values.
+ */
+exports.config = {
+ /**
+ * Array of application names.
+ */
+ app_name: ['Nylas K2 API'],
+ /**
+ * Your New Relic license key.
+ */
+ license_key: 'e232d6ccc786bd87aa72b86782439710162e3739',
+ logging: {
+ /**
+ * Level at which to log. 'trace' is most useful to New Relic when diagnosing
+ * issues with the agent, 'info' and higher will impose the least overhead on
+ * production applications.
+ */
+ level: 'info',
+ },
+}
diff --git a/packages/nylas-api/routes/auth.js b/packages/nylas-api/routes/auth.js
index fd14f4fc5..5cd1e2fb4 100644
--- a/packages/nylas-api/routes/auth.js
+++ b/packages/nylas-api/routes/auth.js
@@ -43,24 +43,33 @@ const buildAccountWith = ({name, email, provider, settings, credentials}) => {
return DatabaseConnector.forShared().then((db) => {
const {AccountToken, Account} = db;
- const account = Account.build({
- name: name,
- provider: provider,
- emailAddress: email,
- connectionSettings: settings,
- syncPolicy: SyncPolicy.defaultPolicy(),
- lastSyncCompletions: [],
- })
- account.setCredentials(credentials);
+ return Account.find({
+ where: {
+ emailAddress: email,
+ connectionSettings: JSON.stringify(settings),
+ },
+ }).then((existing) => {
+ const account = existing || Account.build({
+ name: name,
+ provider: provider,
+ emailAddress: email,
+ connectionSettings: settings,
+ syncPolicy: SyncPolicy.defaultPolicy(),
+ lastSyncCompletions: [],
+ })
- return account.save().then((saved) =>
- AccountToken.create({accountId: saved.id}).then((token) =>
- DatabaseConnector.prepareAccountDatabase(saved.id).thenReturn({
- account: saved,
- token: token,
- })
- )
- );
+ // always update with the latest credentials
+ account.setCredentials(credentials);
+
+ return account.save().then((saved) =>
+ AccountToken.create({accountId: saved.id}).then((token) =>
+ DatabaseConnector.prepareAccountDatabase(saved.id).thenReturn({
+ account: saved,
+ token: token,
+ })
+ )
+ );
+ });
});
}
@@ -97,7 +106,11 @@ module.exports = (server) => {
const {settings, email, provider, name} = request.payload;
if (provider === 'imap') {
- connectionChecks.push(IMAPConnection.connect(dbStub, settings))
+ connectionChecks.push(IMAPConnection.connect({
+ logger: request.logger,
+ settings: settings,
+ db: dbStub,
+ }));
}
Promise.all(connectionChecks).then(() => {
@@ -186,9 +199,12 @@ module.exports = (server) => {
client_id: GMAIL_CLIENT_ID,
client_secret: GMAIL_CLIENT_SECRET,
}
-
Promise.all([
- IMAPConnection.connect({}, Object.assign({}, settings, credentials)),
+ IMAPConnection.connect({
+ logger: request.logger,
+ settings: Object.assign({}, settings, credentials),
+ db: {},
+ }),
])
.then(() =>
buildAccountWith({
diff --git a/packages/nylas-api/routes/files.js b/packages/nylas-api/routes/files.js
index e22975daf..46f7ee980 100644
--- a/packages/nylas-api/routes/files.js
+++ b/packages/nylas-api/routes/files.js
@@ -73,9 +73,7 @@ module.exports = (server) => {
},
handler: (request, reply) => {
request.getAccountDatabase().then(({File}) => {
- const {headers: {accept}} = request
const {params: {id}} = request
- const account = request.auth.credentials
File.findOne({where: {id}}).then((file) => {
if (!file) {
@@ -83,9 +81,9 @@ module.exports = (server) => {
}
return reply(Serialization.jsonStringify(file));
})
- .catch((error) => {
- console.log('Error fetching file: ', error)
- reply(error)
+ .catch((err) => {
+ request.logger.error(err, 'Error fetching file')
+ reply(err)
})
})
},
@@ -107,7 +105,6 @@ module.exports = (server) => {
handler: (request, reply) => {
request.getAccountDatabase()
.then((db) => {
- const {headers: {accept}} = request
const {params: {id}} = request
const account = request.auth.credentials
@@ -116,12 +113,12 @@ module.exports = (server) => {
if (!file) {
return reply.notFound(`File ${id} not found`)
}
- return file.fetch({account, db})
+ return file.fetch({account, db, logger: request.logger})
.then((stream) => reply(stream))
})
- .catch((error) => {
- console.log('Error fetching file: ', error)
- reply(error)
+ .catch((err) => {
+ request.logger.error(err, 'Error downloading file')
+ reply(err)
})
})
},
diff --git a/packages/nylas-api/routes/messages.js b/packages/nylas-api/routes/messages.js
index c5efda964..474f773fe 100644
--- a/packages/nylas-api/routes/messages.js
+++ b/packages/nylas-api/routes/messages.js
@@ -123,15 +123,16 @@ module.exports = (server) => {
return reply.notFound(`Message ${id} not found`)
}
if (accept === 'message/rfc822') {
- return message.fetchRaw({account, db}).then((rawMessage) =>
+ return message.fetchRaw({account, db, logger: request.logger})
+ .then((rawMessage) =>
reply(rawMessage)
)
}
return reply(Serialization.jsonStringify(message));
})
- .catch((error) => {
- console.log('Error fetching message: ', error)
- reply(error)
+ .catch((err) => {
+ request.logger.error(err, 'Error fetching message')
+ reply(err)
})
})
},
diff --git a/packages/nylas-api/routes/ping.js b/packages/nylas-api/routes/ping.js
index a2c9e82e4..0e7072f78 100644
--- a/packages/nylas-api/routes/ping.js
+++ b/packages/nylas-api/routes/ping.js
@@ -6,8 +6,8 @@ module.exports = (server) => {
auth: false,
},
handler: (request, reply) => {
- console.log("---> Ping!")
- reply("pong")
+ request.logger.info('----> Pong!')
+ reply("Pong")
},
});
};
diff --git a/packages/nylas-api/serialization.js b/packages/nylas-api/serialization.js
index cf5e0d68f..f01d9d757 100644
--- a/packages/nylas-api/serialization.js
+++ b/packages/nylas-api/serialization.js
@@ -23,7 +23,7 @@ function jsonSchema(modelName) {
// connection_settings: Joi.object(),
// sync_policy: Joi.object(),
// sync_error: Joi.object().allow(null),
- // first_sync_completed_at: Joi.number().allow(null),
+ // first_sync_completion: Joi.number().allow(null),
// last_sync_completions: Joi.array(),
// })
}
diff --git a/packages/nylas-core/imap-connection.js b/packages/nylas-core/imap-connection.js
index ac1eedb73..c692f11bf 100644
--- a/packages/nylas-core/imap-connection.js
+++ b/packages/nylas-core/imap-connection.js
@@ -156,8 +156,6 @@ class IMAPBox {
}
return this._imap.closeBoxAsync(expunge)
}
-
-
}
@@ -176,8 +174,17 @@ class IMAPConnection extends EventEmitter {
return new IMAPConnection(...args).connect()
}
- constructor(db, settings) {
+ constructor({db, settings, logger} = {}) {
super();
+
+ if (!(settings instanceof Object)) {
+ throw new Error("IMAPConnection: Must be instantiated with `settings`")
+ }
+ if (!logger) {
+ throw new Error("IMAPConnection: Must be instantiated with `logger`")
+ }
+
+ this._logger = logger;
this._db = db;
this._queue = [];
this._currentOperation = null;
@@ -231,13 +238,13 @@ class IMAPConnection extends EventEmitter {
this._imap = Promise.promisifyAll(new Imap(settings));
this._imap.once('end', () => {
- console.log('Underlying IMAP Connection ended');
+ this._logger.info('Underlying IMAP Connection ended');
this._connectPromise = null;
this._imap = null;
});
this._imap.on('alert', (msg) => {
- console.log(`IMAP SERVER SAYS: ${msg}`)
+ this._logger.info({imap_server_msg: msg}, `IMAP server message`)
})
// Emitted when new mail arrives in the currently open mailbox.
@@ -346,14 +353,20 @@ class IMAPConnection extends EventEmitter {
result.then(() => {
this._currentOperation = null;
- console.log(`Finished task: ${operation.description()}`)
+ this._logger.info({
+ operation_type: operation.constructor.name,
+ operation_description: operation.description(),
+ }, `Finished sync operation`)
resolve();
this.processNextOperation();
})
.catch((err) => {
this._currentOperation = null;
- console.log(`Task errored: ${operation.description()}`)
- console.error(err)
+ this._logger.error({
+ err,
+ operation_type: operation.constructor.name,
+ operation_description: operation.description(),
+ }, `Sync operation errored`)
reject(err);
})
}
diff --git a/packages/nylas-core/index.js b/packages/nylas-core/index.js
index d8ac0b59a..3282ce9a3 100644
--- a/packages/nylas-core/index.js
+++ b/packages/nylas-core/index.js
@@ -11,4 +11,5 @@ module.exports = {
SyncPolicy: require('./sync-policy'),
SchedulerUtils: require('./scheduler-utils'),
MessageTypes: require('./message-types'),
+ Logger: require('./logger'),
}
diff --git a/packages/nylas-core/logger.js b/packages/nylas-core/logger.js
new file mode 100644
index 000000000..1bdcb9002
--- /dev/null
+++ b/packages/nylas-core/logger.js
@@ -0,0 +1,61 @@
+const bunyan = require('bunyan')
+const createCWStream = require('bunyan-cloudwatch')
+const PrettyStream = require('bunyan-prettystream');
+const NODE_ENV = process.env.NODE_ENV || 'unknown'
+
+
+function getLogStreams(name, env) {
+ if (env === 'development') {
+ const prettyStdOut = new PrettyStream();
+ prettyStdOut.pipe(process.stdout);
+ const stdoutStream = {
+ type: 'raw',
+ level: 'debug',
+ stream: prettyStdOut,
+ }
+ return [stdoutStream]
+ }
+
+ const stdoutStream = {
+ stream: process.stdout,
+ level: 'info',
+ }
+ const cloudwatchStream = {
+ stream: createCWStream({
+ logGroupName: `k2-${env}`,
+ logStreamName: `${name}-${env}`,
+ cloudWatchLogsOptions: {
+ region: 'us-east-1',
+ },
+ }),
+ type: 'raw',
+ reemitErrorEvents: true,
+ }
+ return [stdoutStream, cloudwatchStream]
+}
+
+function createLogger(name, env = NODE_ENV) {
+ const childLogs = new Map()
+ const logger = bunyan.createLogger({
+ name,
+ serializers: bunyan.stdSerializers,
+ streams: getLogStreams(name, env),
+ })
+
+ return Object.assign(logger, {
+ forAccount(account = {}) {
+ if (!childLogs.has(account.id)) {
+ const childLog = logger.child({
+ account_id: account.id,
+ account_email: account.emailAddress,
+ })
+ childLogs.set(account.id, childLog)
+ }
+ return childLogs.get(account.id)
+ },
+ })
+}
+
+module.exports = {
+ createLogger,
+}
diff --git a/packages/nylas-core/models/account/file.js b/packages/nylas-core/models/account/file.js
index 5043318e8..655e8b13d 100644
--- a/packages/nylas-core/models/account/file.js
+++ b/packages/nylas-core/models/account/file.js
@@ -15,11 +15,11 @@ module.exports = (sequelize, Sequelize) => {
},
},
instanceMethods: {
- fetch: function fetch({account, db}) {
+ fetch: function fetch({account, db, logger}) {
const settings = Object.assign({}, account.connectionSettings, account.decryptedCredentials())
return Promise.props({
message: this.getMessage(),
- connection: IMAPConnection.connect(db, settings),
+ connection: IMAPConnection.connect({db, settings, logger}),
})
.then(({message, connection}) => {
return message.getFolder()
diff --git a/packages/nylas-core/models/account/message.js b/packages/nylas-core/models/account/message.js
index 80bf1b5b5..e5fffdacc 100644
--- a/packages/nylas-core/models/account/message.js
+++ b/packages/nylas-core/models/account/message.js
@@ -69,11 +69,11 @@ module.exports = (sequelize, Sequelize) => {
)
},
- fetchRaw: function fetchRaw({account, db}) {
+ fetchRaw: function fetchRaw({account, db, logger}) {
const settings = Object.assign({}, account.connectionSettings, account.decryptedCredentials())
return Promise.props({
folder: this.getFolder(),
- connection: IMAPConnection.connect(db, settings),
+ connection: IMAPConnection.connect({db, settings, logger}),
})
.then(({folder, connection}) => {
return connection.openBox(folder.name)
diff --git a/packages/nylas-core/models/shared/account.js b/packages/nylas-core/models/shared/account.js
index e5b203666..9a61101a9 100644
--- a/packages/nylas-core/models/shared/account.js
+++ b/packages/nylas-core/models/shared/account.js
@@ -12,7 +12,7 @@ module.exports = (sequelize, Sequelize) => {
connectionCredentials: Sequelize.TEXT,
syncPolicy: JSONType('syncPolicy'),
syncError: JSONType('syncError', {defaultValue: null}),
- firstSyncCompletedAt: Sequelize.INTEGER,
+ firstSyncCompletion: Sequelize.INTEGER(14),
lastSyncCompletions: JSONARRAYType('lastSyncCompletions'),
}, {
classMethods: {
@@ -31,7 +31,7 @@ module.exports = (sequelize, Sequelize) => {
connection_settings: this.connectionSettings,
sync_policy: this.syncPolicy,
sync_error: this.syncError,
- first_sync_completed_at: this.firstSyncCompletedAt,
+ first_sync_completion: this.firstSyncCompletion,
last_sync_completions: this.lastSyncCompletions,
created_at: this.createdAt,
}
diff --git a/packages/nylas-core/package.json b/packages/nylas-core/package.json
index fa5d9cc27..3c151ff87 100644
--- a/packages/nylas-core/package.json
+++ b/packages/nylas-core/package.json
@@ -4,6 +4,7 @@
"description": "Core shared packages",
"main": "index.js",
"dependencies": {
+ "bunyan": "^1.8.1",
"imap": "0.8.x",
"xoauth2": "1.x.x"
},
diff --git a/packages/nylas-core/pubsub-connector.js b/packages/nylas-core/pubsub-connector.js
index 76df1df5d..36121688a 100644
--- a/packages/nylas-core/pubsub-connector.js
+++ b/packages/nylas-core/pubsub-connector.js
@@ -1,9 +1,11 @@
const Rx = require('rx')
const redis = require("redis");
+const log = global.Logger || console
Promise.promisifyAll(redis.RedisClient.prototype);
Promise.promisifyAll(redis.Multi.prototype);
+
class PubsubConnector {
constructor() {
this._broadcastClient = null;
@@ -13,7 +15,7 @@ class PubsubConnector {
buildClient() {
const client = redis.createClient(process.env.REDIS_URL || null);
- client.on("error", console.error);
+ client.on("error", log.error);
return client;
}
diff --git a/packages/nylas-core/scheduler-utils.js b/packages/nylas-core/scheduler-utils.js
index 55f8a4959..9b719f0ba 100644
--- a/packages/nylas-core/scheduler-utils.js
+++ b/packages/nylas-core/scheduler-utils.js
@@ -22,7 +22,9 @@ const forEachAccountList = (forEachCallback) => {
}
const assignPolicy = (accountId, policy) => {
- console.log(`Changing policy for ${accountId} to ${JSON.stringify(policy)}`)
+ const log = global.Logger || console
+ log.info({policy, account_id: accountId}, `Changing single policy`)
+
const DatabaseConnector = require('./database-connector');
return DatabaseConnector.forShared().then(({Account}) => {
Account.find({where: {id: accountId}}).then((account) => {
@@ -33,7 +35,9 @@ const assignPolicy = (accountId, policy) => {
}
const assignPolicyToAcounts = (accountIds, policy) => {
- console.log(`Changing policy for ${accountIds} to ${JSON.stringify(policy)}`)
+ const log = global.Logger || console
+ log.info({policy, account_ids: accountIds}, `Changing multiple policies`)
+
const DatabaseConnector = require('./database-connector');
return DatabaseConnector.forShared().then(({Account}) => {
Account.findAll({where: {id: {$or: accountIds}}}).then((accounts) => {
diff --git a/packages/nylas-dashboard/app.js b/packages/nylas-dashboard/app.js
index 2ee772b4a..dbd02d606 100644
--- a/packages/nylas-dashboard/app.js
+++ b/packages/nylas-dashboard/app.js
@@ -1,11 +1,12 @@
const Hapi = require('hapi');
const HapiWebSocket = require('hapi-plugin-websocket');
const Inert = require('inert');
-const {DatabaseConnector, PubsubConnector, SchedulerUtils} = require(`nylas-core`);
+const {DatabaseConnector, PubsubConnector, SchedulerUtils, Logger} = require(`nylas-core`);
const fs = require('fs');
const path = require('path');
global.Promise = require('bluebird');
+global.Logger = Logger.createLogger('nylas-k2-dashboard')
const server = new Hapi.Server();
server.connection({ port: process.env.PORT });
@@ -98,6 +99,6 @@ server.register([HapiWebSocket, Inert], () => {
server.start((startErr) => {
if (startErr) { throw startErr; }
- console.log('Dashboard running at:', server.info.uri);
+ global.Logger.info({uri: server.info.uri}, 'Dashboard running');
});
});
diff --git a/packages/nylas-dashboard/public/css/app.css b/packages/nylas-dashboard/public/css/app.css
index ceb513342..436426196 100644
--- a/packages/nylas-dashboard/public/css/app.css
+++ b/packages/nylas-dashboard/public/css/app.css
@@ -59,10 +59,14 @@ pre {
.action-link {
display: inline-block;
- margin: 5px;
color: rgba(16, 83, 161, 0.88);
text-decoration: underline;
cursor: pointer;
+ margin: 5px 0;
+}
+
+.action-link.cancel {
+ margin-left: 5px;
}
.sync-policy textarea {
@@ -76,6 +80,7 @@ pre {
width: 50%;
margin: auto;
padding: 20px;
+ overflow: auto;
}
.modal-bg {
@@ -88,6 +93,15 @@ pre {
padding-top: 10%;
}
+.modal-close {
+ position: relative;
+ float: right;
+ top: -10px;
+ cursor: pointer;
+ font-size: 14px;
+ font-weight: bold;
+}
+
.sync-graph {
margin-top: 3px;
}
@@ -97,3 +111,65 @@ pre {
margin-top: 5px;
margin-bottom: 1px;
}
+
+#syncback-request-details {
+ font-size: 15px;
+ color: black;
+}
+
+#syncback-request-details .counts {
+ margin: 10px;
+}
+
+#syncback-request-details span {
+ margin: 10px;
+}
+
+#syncback-request-details table {
+ width: 100%;
+}
+
+#syncback-request-details tr:nth-child(even) {
+ background-color: #F1F1F1;
+}
+
+#syncback-request-details tr:not(:first-child):hover {
+ background-color: #C9C9C9;
+}
+
+#syncback-request-details td, #syncback-request-details th {
+ text-align: center;
+ padding: 10px 5px;
+}
+
+.dropdown-arrow {
+ margin: 0 5px;
+ height: 7px;
+ vertical-align: middle;
+}
+
+.dropdown-options {
+ border: solid black 1px;
+ position: absolute;
+ background-color: white;
+ text-align: left;
+}
+
+.dropdown-option {
+ padding: 0px 2px;
+}
+
+.dropdown-option:hover {
+ background-color: rgb(114, 163, 255);
+}
+
+.dropdown-selected {
+ display: inline;
+}
+
+.dropdown-wrapper {
+ display: inline;
+ cursor: pointer;
+ position: absolute;
+ font-weight: normal;
+}
diff --git a/packages/nylas-dashboard/public/images/dropdown.png b/packages/nylas-dashboard/public/images/dropdown.png
new file mode 100644
index 000000000..a49a693f9
Binary files /dev/null and b/packages/nylas-dashboard/public/images/dropdown.png differ
diff --git a/packages/nylas-dashboard/public/index.html b/packages/nylas-dashboard/public/index.html
index 89227c3d8..a1c31b217 100644
--- a/packages/nylas-dashboard/public/index.html
+++ b/packages/nylas-dashboard/public/index.html
@@ -7,6 +7,8 @@
+
+
diff --git a/packages/nylas-dashboard/public/js/app.jsx b/packages/nylas-dashboard/public/js/app.jsx
index 5f740093b..ecf7a1f7d 100644
--- a/packages/nylas-dashboard/public/js/app.jsx
+++ b/packages/nylas-dashboard/public/js/app.jsx
@@ -1,4 +1,6 @@
/* eslint react/react-in-jsx-scope: 0*/
+/* eslint no-console: 0*/
+
const React = window.React;
const ReactDOM = window.ReactDOM;
const {
@@ -6,6 +8,7 @@ const {
SetAllSyncPolicies,
AccountFilter,
SyncGraph,
+ SyncbackRequestDetails,
} = window;
class Account extends React.Component {
@@ -43,14 +46,15 @@ class Account extends React.Component {
const timeSinceLastSync = (Date.now() - newestSync) / 1000;
let firstSyncDuration = "Incomplete";
- if (account.first_sync_completed_at) {
- firstSyncDuration = (new Date(account.first_sync_completed_at) - new Date(account.created_at)) / 1000;
+ if (account.first_sync_completion) {
+ firstSyncDuration = (new Date(account.first_sync_completion) - new Date(account.created_at)) / 1000;
}
return (
{account.email_address} {active ? '🌕' : '🌑'}
{assignment}
+
selectedOnClick.call(this)}>
+ {this.state.selected}
+
+
+ );
+
+ // All options, not shown if dropdown is closed
+ let options = [];
+ let optionsWrapper = ;
+ if (!this.state.closed) {
+ for (const opt of this.props.options) {
+ options.push(
+ this.selectAndClose.call(this, opt)}> {opt}
+ );
+ }
+ optionsWrapper = (
+
+ {options}
+
+ )
+ }
+
+ return (
+ this.close.call(this)}>
+ {selected}
+ {optionsWrapper}
+
+ );
+ }
+
+}
+
+Dropdown.propTypes = {
+ options: React.PropTypes.arrayOf(React.PropTypes.string),
+ defaultOption: React.PropTypes.string,
+ onSelect: React.PropTypes.func,
+}
+
+window.Dropdown = Dropdown;
diff --git a/packages/nylas-dashboard/public/js/set-all-sync-policies.jsx b/packages/nylas-dashboard/public/js/set-all-sync-policies.jsx
index f9346c1e6..06bfe2e26 100644
--- a/packages/nylas-dashboard/public/js/set-all-sync-policies.jsx
+++ b/packages/nylas-dashboard/public/js/set-all-sync-policies.jsx
@@ -38,15 +38,20 @@ class SetAllSyncPolicies extends React.Component {
render() {
if (this.state.editMode) {
return (
-
-
-
Sync Policy
-
-
this.applyToAllAccounts.call(this, this.props.accountIds)}>
- Apply To All Displayed Accounts
-
-
this.cancel.call(this)}> Cancel
+
+
this.edit.call(this)}>
+ Set sync policies for currently displayed accounts
+
+
+
+
Sync Policy
+
+
this.applyToAllAccounts.call(this, this.props.accountIds)}>
+ Apply To All Displayed Accounts
+
+
this.cancel.call(this)}> Cancel
+
)
diff --git a/packages/nylas-dashboard/public/js/sync-policy.jsx b/packages/nylas-dashboard/public/js/sync-policy.jsx
index bfbee8ffb..5efe9655b 100644
--- a/packages/nylas-dashboard/public/js/sync-policy.jsx
+++ b/packages/nylas-dashboard/public/js/sync-policy.jsx
@@ -43,7 +43,7 @@ class SyncPolicy extends React.Component {
{this.props.stringifiedSyncPolicy}
this.save.call(this)}> Save
-
this.cancel.call(this)}> Cancel
+
this.cancel.call(this)}> Cancel
)
diff --git a/packages/nylas-dashboard/public/js/syncback-request-details.jsx b/packages/nylas-dashboard/public/js/syncback-request-details.jsx
new file mode 100644
index 000000000..1aecdb220
--- /dev/null
+++ b/packages/nylas-dashboard/public/js/syncback-request-details.jsx
@@ -0,0 +1,171 @@
+const React = window.React;
+const Dropdown = window.Dropdown;
+
+class SyncbackRequestDetails extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ open: false,
+ accountId: props.accountId,
+ syncbackRequests: null,
+ counts: null,
+ statusFilter: 'all',
+ };
+ }
+
+ getDetails() {
+ const req = new XMLHttpRequest();
+ const url = `${window.location.protocol}/syncback-requests/${this.state.accountId}`;
+ req.open("GET", url, true);
+ req.onreadystatechange = () => {
+ if (req.readyState === XMLHttpRequest.DONE) {
+ if (req.status === 200) {
+ this.setState({syncbackRequests: req.responseText});
+ } else {
+ console.error(req.responseText);
+ }
+ }
+ }
+ req.send();
+ }
+
+ getCounts() {
+ const since = Date.now() - 1000 * 60 * 60; // one hour ago
+ const req = new XMLHttpRequest();
+ const url = `${window.location.protocol}/syncback-requests/${this.state.accountId}/counts?since=${since}`;
+ req.open("GET", url, true);
+ req.onreadystatechange = () => {
+ if (req.readyState === XMLHttpRequest.DONE) {
+ if (req.status === 200) {
+ this.setState({counts: JSON.parse(req.responseText)});
+ } else {
+ console.error(req.responseText);
+ }
+ }
+ }
+ req.send();
+ }
+
+ setStatusFilter(statusFilter) {
+ this.setState({statusFilter: statusFilter});
+ }
+
+ open() {
+ this.getDetails();
+ this.getCounts();
+ this.setState({open: true});
+ }
+
+ close() {
+ this.setState({open: false});
+ }
+
+ render() {
+ if (this.state.open) {
+ let counts =
Of requests created in the last hour: ...
+ if (this.state.counts) {
+ const total = this.state.counts.new + this.state.counts.failed
+ + this.state.counts.succeeded;
+ if (total === 0) {
+ counts = "No requests made in the last hour";
+ } else {
+ counts = (
+
+ Of requests created in the last hour:
+
+ {this.state.counts.failed / total * 100}% failed
+
+
+ {this.state.counts.succeeded / total * 100}% succeeded
+
+
+ {/* .new was throwing off my syntax higlighting, so ignoring linter*/}
+ {this.state.counts['new'] / total * 100}% are still new
+
+
+ )
+ }
+ }
+
+ let details = "Loading..."
+ if (this.state.syncbackRequests) {
+ let reqs = JSON.parse(this.state.syncbackRequests);
+ if (this.state.statusFilter !== 'all') {
+ reqs = reqs.filter((req) => req.status === this.state.statusFilter);
+ }
+ let rows = [];
+ if (reqs.length === 0) {
+ rows.push(
No results - - );
+ }
+ for (let i = reqs.length - 1; i >= 0; i--) {
+ const req = reqs[i];
+ const date = new Date(req.createdAt);
+ rows.push(
+ {req.status}
+ {req.type}
+ {date.toLocaleTimeString()}, {date.toLocaleDateString()}
+ )
+ }
+ details = (
+
+
+
+
+ Status:
+ this.setStatusFilter.call(this, status)}
+ />
+
+ Type
+ Created At
+
+ {rows}
+
+
+ );
+ }
+
+ return (
+
+
Syncback Request Details
+
+
+
this.close.call(this)}>
+ X
+
+
+ {counts}
+ {details}
+
+
+
+
+ );
+ }
+ // else, the modal isn't open
+ return (
+
+ this.open.call(this)}>
+ Syncback Request Details
+
+
+ );
+ }
+}
+
+SyncbackRequestDetails.propTypes = {
+ accountId: React.PropTypes.number,
+}
+
+window.SyncbackRequestDetails = SyncbackRequestDetails;
diff --git a/packages/nylas-dashboard/routes/syncback-requests.js b/packages/nylas-dashboard/routes/syncback-requests.js
new file mode 100644
index 000000000..267f1086a
--- /dev/null
+++ b/packages/nylas-dashboard/routes/syncback-requests.js
@@ -0,0 +1,81 @@
+const Joi = require('joi');
+const {DatabaseConnector} = require(`nylas-core`);
+
+module.exports = (server) => {
+ server.route({
+ method: 'GET',
+ path: '/syncback-requests/{account_id}',
+ config: {
+ description: 'Get the SyncbackRequests for an account',
+ notes: 'Notes go here',
+ tags: ['syncback-requests'],
+ validate: {
+ params: {
+ account_id: Joi.number().integer(),
+ },
+ },
+ response: {
+ schema: Joi.string(),
+ },
+ },
+ handler: (request, reply) => {
+ DatabaseConnector.forAccount(request.params.account_id).then((db) => {
+ const {SyncbackRequest} = db;
+ SyncbackRequest.findAll().then((syncbackRequests) => {
+ reply(JSON.stringify(syncbackRequests))
+ });
+ });
+ },
+ });
+
+ server.route({
+ method: 'GET',
+ path: '/syncback-requests/{account_id}/counts',
+ config: {
+ description: 'Get stats on the statuses of SyncbackRequests',
+ notes: 'Notes go here',
+ tags: ['syncback-requests'],
+ validate: {
+ params: {
+ account_id: Joi.number().integer(),
+ },
+ query: {
+ since: Joi.date().timestamp(),
+ },
+ },
+ response: {
+ schema: Joi.string(),
+ },
+ },
+ handler: (request, reply) => {
+ DatabaseConnector.forAccount(request.params.account_id).then((db) => {
+ const {SyncbackRequest} = db;
+
+ const counts = {
+ 'new': null,
+ 'succeeded': null,
+ 'failed': null,
+ }
+
+ const where = {};
+ if (request.query.since) {
+ where.createdAt = {gt: request.query.since};
+ }
+
+ const countPromises = [];
+ for (const status of Object.keys(counts)) {
+ where.status = status.toUpperCase();
+ countPromises.push(
+ SyncbackRequest.count({where: where}).then((count) => {
+ counts[status] = count;
+ })
+ );
+ }
+
+ Promise.all(countPromises).then(() => {
+ reply(JSON.stringify(counts));
+ })
+ });
+ },
+ });
+};
diff --git a/packages/nylas-message-processor/app.js b/packages/nylas-message-processor/app.js
index 458a074e3..4ad7aee14 100644
--- a/packages/nylas-message-processor/app.js
+++ b/packages/nylas-message-processor/app.js
@@ -1,7 +1,8 @@
-const {PubsubConnector, DatabaseConnector} = require(`nylas-core`)
+const {PubsubConnector, DatabaseConnector, Logger} = require(`nylas-core`)
const {processors} = require('./processors')
global.Promise = require('bluebird');
+global.Logger = Logger.createLogger('nylas-k2-message-processor')
// List of the attributes of Message that the processor should be allowed to change.
// The message may move between folders, get starred, etc. while it's being
@@ -11,15 +12,13 @@ const MessageProcessorVersion = 1;
const redis = PubsubConnector.buildClient();
-function runPipeline({db, accountId, message}) {
- console.log(`Processing message ${message.id}`)
+function runPipeline({db, accountId, message, logger}) {
+ logger.info(`MessageProcessor: Processing message`)
return processors.reduce((prevPromise, processor) => (
prevPromise.then((prevMessage) => {
- const processed = processor({message: prevMessage, accountId, db});
- if (!(processed instanceof Promise)) {
- throw new Error(`processor ${processor} did not return a promise.`)
- }
- return processed.then((nextMessage) => {
+ const processed = processor({message: prevMessage, accountId, db, logger});
+ return Promise.resolve(processed)
+ .then((nextMessage) => {
if (!nextMessage.body) {
throw new Error("processor did not resolve with a valid message object.")
}
@@ -46,26 +45,28 @@ function dequeueJob() {
try {
json = JSON.parse(item[1]);
} catch (error) {
- console.error(`MessageProcessor Failed: Found invalid JSON item in queue: ${item}`)
+ global.Logger.error({item}, `MessageProcessor: Found invalid JSON item in queue`)
return dequeueJob();
}
const {messageId, accountId} = json;
+ const logger = global.Logger.forAccount({id: accountId}).child({message_id: messageId})
- DatabaseConnector.forAccount(accountId).then((db) =>
- db.Message.find({
+ DatabaseConnector.forAccount(accountId).then((db) => {
+ return db.Message.find({
where: {id: messageId},
include: [{model: db.Folder}, {model: db.Label}],
}).then((message) => {
if (!message) {
return Promise.reject(new Error(`Message not found (${messageId}). Maybe account was deleted?`))
}
- return runPipeline({db, accountId, message}).then((processedMessage) =>
+ return runPipeline({db, accountId, message, logger}).then((processedMessage) =>
saveMessage(processedMessage)
).catch((err) =>
- console.error(`MessageProcessor Failed: ${err} ${err.stack}`)
+ logger.error(err, `MessageProcessor: Failed`)
)
})
- ).finally(() => {
+ })
+ .finally(() => {
dequeueJob()
});
diff --git a/packages/nylas-message-processor/processors/parsing.js b/packages/nylas-message-processor/processors/parsing.js
index 6763437e0..a144199ab 100644
--- a/packages/nylas-message-processor/processors/parsing.js
+++ b/packages/nylas-message-processor/processors/parsing.js
@@ -11,7 +11,7 @@ function Contact({name, address} = {}) {
const extractContacts = (values) =>
(values || []).map(v => Contact(mimelib.parseAddresses(v).pop()))
-function processMessage({message}) {
+function processMessage({message, logger}) {
if (message.snippet) {
// trim and clean snippet which is alreay present (from message plaintext)
message.snippet = message.snippet.replace(/[\n\r]/g, ' ').replace(/\s\s+/g, ' ')
@@ -24,7 +24,7 @@ function processMessage({message}) {
// TODO: Fanciness
message.snippet = message.body.substr(0, Math.min(message.body.length, SNIPPET_SIZE));
} else {
- console.log("Received message has no body or snippet.")
+ logger.info("MessageProcessor: Parsing - Received message has no body or snippet.")
}
message.to = extractContacts(message.headers.to);
diff --git a/packages/nylas-message-processor/processors/threading.js b/packages/nylas-message-processor/processors/threading.js
index 57e896ec2..2f9c847fd 100644
--- a/packages/nylas-message-processor/processors/threading.js
+++ b/packages/nylas-message-processor/processors/threading.js
@@ -9,11 +9,14 @@ class ThreadingProcessor {
// conversation. Put it back soonish.
// const messageEmails = _.uniq([].concat(message.to, message.cc, message.from).map(p => p.email));
- // console.log(`Found ${threads.length} candidate threads for message with subject: ${message.subject}`)
+ // this.logger.info({
+ // num_candidate_threads: threads.length,
+ // message_subject: message.subject,
+ // }, `Found candidate threads for message`)
//
// for (const thread of threads) {
// const threadEmails = _.uniq([].concat(thread.participants).map(p => p.email));
- // console.log(`Intersection: ${_.intersection(threadEmails, messageEmails).join(',')}`)
+ // this.logger.info(`Intersection: ${_.intersection(threadEmails, messageEmails).join(',')}`)
//
// if (_.intersection(threadEmails, messageEmails) >= threadEmails.length * 0.9) {
// return thread;
@@ -66,7 +69,7 @@ class ThreadingProcessor {
})
}
- processMessage({db, message}) {
+ processMessage({db, message, logger}) {
if (!(message.labels instanceof Array)) {
throw new Error("Threading processMessage expects labels to be an inflated array.");
}
@@ -74,6 +77,8 @@ class ThreadingProcessor {
throw new Error("Threading processMessage expects folder value to be present.");
}
+ this.logger = logger
+
const {Folder, Label} = db;
let findOrCreateThread = null;
if (message.headers['x-gm-thrid']) {
diff --git a/packages/nylas-message-processor/spec/threading-spec.js b/packages/nylas-message-processor/spec/threading-spec.js
index b1df5d815..5e0aaa3ba 100644
--- a/packages/nylas-message-processor/spec/threading-spec.js
+++ b/packages/nylas-message-processor/spec/threading-spec.js
@@ -43,7 +43,6 @@ it('adds the message to the thread', (done) => {
},
create: (message) => {
message.setThread = (thread) => {
- console.log("setting")
message.thread = thread.id
}
return Promise.resolve(message)
diff --git a/packages/nylas-sync/app.js b/packages/nylas-sync/app.js
index 5d7319245..ac81a02f1 100644
--- a/packages/nylas-sync/app.js
+++ b/packages/nylas-sync/app.js
@@ -1,16 +1,18 @@
+// require('newrelic');
global.Promise = require('bluebird');
-
-const {DatabaseConnector} = require(`nylas-core`)
+const {DatabaseConnector, Logger} = require(`nylas-core`)
const SyncProcessManager = require('./sync-process-manager');
+global.Logger = Logger.createLogger('nylas-k2-sync')
+
const manager = new SyncProcessManager();
DatabaseConnector.forShared().then((db) => {
const {Account} = db;
Account.findAll().then((accounts) => {
if (accounts.length === 0) {
- console.log(`Couldn't find any accounts to sync. Run this CURL command to auth one!`)
- console.log(`curl -X POST -H "Content-Type: application/json" -d '{"email":"inboxapptest2@fastmail.fm", "name":"Ben Gotow", "provider":"imap", "settings":{"imap_username":"inboxapptest1@fastmail.fm","imap_host":"mail.messagingengine.com","imap_port":993,"smtp_host":"mail.messagingengine.com","smtp_port":0,"smtp_username":"inboxapptest1@fastmail.fm", "smtp_password":"trar2e","imap_password":"trar2e","ssl_required":true}}' "http://localhost:5100/auth?client_id=123"`)
+ global.Logger.info(`Couldn't find any accounts to sync. Run this CURL command to auth one!`)
+ global.Logger.info(`curl -X POST -H "Content-Type: application/json" -d '{"email":"inboxapptest1@fastmail.fm", "name":"Ben Gotow", "provider":"imap", "settings":{"imap_username":"inboxapptest1@fastmail.fm","imap_host":"mail.messagingengine.com","imap_port":993,"smtp_host":"mail.messagingengine.com","smtp_port":0,"smtp_username":"inboxapptest1@fastmail.fm", "smtp_password":"trar2e","imap_password":"trar2e","ssl_required":true}}' "http://localhost:5100/auth?client_id=123"`)
}
manager.ensureAccountIDsInRedis(accounts.map(a => a.id)).then(() => {
manager.start();
diff --git a/packages/nylas-sync/imap/fetch-category-list.js b/packages/nylas-sync/imap/fetch-folder-list.js
similarity index 97%
rename from packages/nylas-sync/imap/fetch-category-list.js
rename to packages/nylas-sync/imap/fetch-folder-list.js
index b82f33876..d5c5a0000 100644
--- a/packages/nylas-sync/imap/fetch-category-list.js
+++ b/packages/nylas-sync/imap/fetch-folder-list.js
@@ -3,8 +3,9 @@ const {Provider} = require('nylas-core');
const GMAIL_FOLDERS = ['[Gmail]/All Mail', '[Gmail]/Trash', '[Gmail]/Spam'];
class FetchFolderList {
- constructor(provider) {
+ constructor(provider, logger = console) {
this._provider = provider;
+ this._logger = logger;
}
description() {
diff --git a/packages/nylas-sync/imap/fetch-messages-in-category.js b/packages/nylas-sync/imap/fetch-messages-in-folder.js
similarity index 84%
rename from packages/nylas-sync/imap/fetch-messages-in-category.js
rename to packages/nylas-sync/imap/fetch-messages-in-folder.js
index 554f09e71..038dde66d 100644
--- a/packages/nylas-sync/imap/fetch-messages-in-category.js
+++ b/packages/nylas-sync/imap/fetch-messages-in-folder.js
@@ -11,12 +11,13 @@ const FETCH_MESSAGES_FIRST_COUNT = 100;
const FETCH_MESSAGES_COUNT = 200;
class FetchMessagesInFolder {
- constructor(category, options) {
+ constructor(category, options, logger = console) {
this._imap = null
this._box = null
this._db = null
this._category = category;
this._options = options;
+ this._logger = logger;
if (!this._category) {
throw new Error("FetchMessagesInFolder requires a category")
}
@@ -86,9 +87,13 @@ class FetchMessagesInFolder {
}
})
- console.log(` --- found ${flagChangeMessages.length || 'no'} flag changes`)
+ this._logger.info({
+ flag_changes: flagChangeMessages.length,
+ }, `FetchMessagesInFolder: found flag changes`)
if (createdUIDs.length > 0) {
- console.log(` --- found ${createdUIDs.length} new messages. These will not be processed because we assume that they will be assigned uid = uidnext, and will be picked up in the next sync when we discover unseen messages.`)
+ this._logger.info({
+ new_messages: createdUIDs.length,
+ }, `FetchMessagesInFolder: found new messages. These will not be processed because we assume that they will be assigned uid = uidnext, and will be picked up in the next sync when we discover unseen messages.`)
}
if (flagChangeMessages.length === 0) {
@@ -111,7 +116,9 @@ class FetchMessagesInFolder {
.filter(msg => !remoteUIDAttributes[msg.folderImapUID])
.map(msg => msg.folderImapUID)
- console.log(` --- found ${removedUIDs.length} messages no longer in the folder`)
+ this._logger.info({
+ removed_messages: removedUIDs.length,
+ }, `FetchMessagesInFolder: found messages no longer in the folder`)
if (removedUIDs.length === 0) {
return Promise.resolve();
@@ -148,7 +155,9 @@ class FetchMessagesInFolder {
}
if (desired.length === 0) {
- console.warn(`Could not find good part. Options are: ${available.join(', ')}`)
+ this._logger.warn({
+ available_options: available.join(', '),
+ }, `FetchMessagesInFolder: Could not find good part`)
}
return desired;
@@ -173,7 +182,10 @@ class FetchMessagesInFolder {
const uids = uidsByPart[key];
const desiredParts = JSON.parse(key);
const bodies = ['HEADER'].concat(desiredParts.map(p => p.id));
- console.log(`Fetching parts ${key} for ${uids.length} messages`)
+ this._logger.info({
+ key,
+ num_messages: uids.length,
+ }, `FetchMessagesInFolder: Fetching parts for messages`)
// note: the order of UIDs in the array doesn't matter, Gmail always
// returns them in ascending (oldest => newest) order.
@@ -258,11 +270,17 @@ class FetchMessagesInFolder {
)
.then((message) => {
if (created) {
- console.log(`Created message ID: ${message.id}, UID: ${attributes.uid}`)
+ this._logger.info({
+ message_id: message.id,
+ uid: attributes.uid,
+ }, `FetchMessagesInFolder: Created message`)
this._createFilesFromStruct({message, struct: attributes.struct})
PubsubConnector.queueProcessMessage({accountId, messageId: message.id});
} else {
- console.log(`Updated message ID: ${message.id}, UID: ${attributes.uid}`)
+ this._logger.info({
+ message_id: message.id,
+ uid: attributes.uid,
+ }, `FetchMessagesInFolder: Updated message`)
}
})
@@ -291,7 +309,9 @@ class FetchMessagesInFolder {
const desiredRanges = [];
- console.log(` - Fetching messages. Currently have range: ${savedSyncState.fetchedmin}:${savedSyncState.fetchedmax}`)
+ this._logger.info({
+ range: `${savedSyncState.fetchedmin}:${savedSyncState.fetchedmax}`,
+ }, `FetchMessagesInFolder: Fetching messages.`)
// Todo: In the future, this is where logic should go that limits
// sync based on number of messages / age of messages.
@@ -303,18 +323,20 @@ class FetchMessagesInFolder {
if (savedSyncState.fetchedmax < boxUidnext) {
desiredRanges.push({min: savedSyncState.fetchedmax, max: boxUidnext})
} else {
- console.log(" --- fetchedmax == uidnext, nothing more recent to fetch.")
+ this._logger.info('FetchMessagesInFolder: fetchedmax == uidnext, nothing more recent to fetch.')
}
if (savedSyncState.fetchedmin > 1) {
const lowerbound = Math.max(1, savedSyncState.fetchedmin - FETCH_MESSAGES_COUNT);
desiredRanges.push({min: lowerbound, max: savedSyncState.fetchedmin})
} else {
- console.log(" --- fetchedmin == 1, nothing older to fetch.")
+ this._logger.info("FetchMessagesInFolder: fetchedmin == 1, nothing older to fetch.")
}
}
return Promise.each(desiredRanges, ({min, max}) => {
- console.log(` --- fetching range: ${min}:${max}`);
+ this._logger.info({
+ range: `${min}:${max}`,
+ }, `FetchMessagesInFolder: Fetching range`);
return this._fetchMessagesAndQueueForProcessing(`${min}:${max}`).then(() => {
const {fetchedmin, fetchedmax} = this._category.syncState;
@@ -326,7 +348,7 @@ class FetchMessagesInFolder {
});
})
}).then(() => {
- console.log(` - Fetching messages finished`);
+ this._logger.info(`FetchMessagesInFolder: Fetching messages finished`);
});
}
@@ -350,15 +372,15 @@ class FetchMessagesInFolder {
let shallowFetch = null;
if (this._imap.serverSupports(Capabilities.Condstore)) {
- console.log(` - Shallow attribute scan (using CONDSTORE)`)
+ this._logger.info(`FetchMessagesInFolder: Shallow attribute scan (using CONDSTORE)`)
if (nextHighestmodseq === highestmodseq) {
- console.log(" --- highestmodseq matches, nothing more to fetch")
+ this._logger.info('FetchMessagesInFolder: highestmodseq matches, nothing more to fetch')
return Promise.resolve();
}
shallowFetch = this._box.fetchUIDAttributes(`1:*`, {changedsince: highestmodseq});
} else {
const range = `${this._getLowerBoundUID(SHALLOW_SCAN_UID_COUNT)}:*`;
- console.log(` - Shallow attribute scan (using range: ${range})`)
+ this._logger.info({range}, `FetchMessagesInFolder: Shallow attribute scan`)
shallowFetch = this._box.fetchUIDAttributes(range);
}
@@ -372,7 +394,7 @@ class FetchMessagesInFolder {
this._updateMessageAttributes(remoteUIDAttributes, localMessageAttributes)
))
.then(() => {
- console.log(` - finished fetching changes to messages`);
+ this._logger.info(`FetchMessagesInFolder: finished fetching changes to messages`);
return this.updateFolderSyncState({
highestmodseq: nextHighestmodseq,
timeShallowScan: Date.now(),
@@ -386,7 +408,7 @@ class FetchMessagesInFolder {
const {fetchedmin, fetchedmax} = this._category.syncState;
const range = `${fetchedmin}:${fetchedmax}`;
- console.log(` - Deep attribute scan: fetching attributes in range: ${range}`)
+ this._logger.info({range}, `FetchMessagesInFolder: Deep attribute scan: fetching attributes in range`)
return this._box.fetchUIDAttributes(range)
.then((remoteUIDAttributes) => {
@@ -401,7 +423,7 @@ class FetchMessagesInFolder {
})
))
.then(() => {
- console.log(` - Deep scan finished.`);
+ this._logger.info(`FetchMessagesInFolder: Deep scan finished.`);
return this.updateFolderSyncState({
highestmodseq: this._box.highestmodseq,
timeDeepScan: Date.now(),
diff --git a/packages/nylas-sync/newrelic.js b/packages/nylas-sync/newrelic.js
new file mode 100644
index 000000000..a85120e87
--- /dev/null
+++ b/packages/nylas-sync/newrelic.js
@@ -0,0 +1,24 @@
+/**
+ * New Relic agent configuration.
+ *
+ * See lib/config.defaults.js in the agent distribution for a more complete
+ * description of configuration variables and their potential values.
+ */
+exports.config = {
+ /**
+ * Array of application names.
+ */
+ app_name: ['Nylas K2 Sync'],
+ /**
+ * Your New Relic license key.
+ */
+ license_key: 'e232d6ccc786bd87aa72b86782439710162e3739',
+ logging: {
+ /**
+ * Level at which to log. 'trace' is most useful to New Relic when diagnosing
+ * issues with the agent, 'info' and higher will impose the least overhead on
+ * production applications.
+ */
+ level: 'info',
+ },
+}
diff --git a/packages/nylas-sync/sync-process-manager.js b/packages/nylas-sync/sync-process-manager.js
index 513bd1664..8d4042895 100644
--- a/packages/nylas-sync/sync-process-manager.js
+++ b/packages/nylas-sync/sync-process-manager.js
@@ -40,10 +40,11 @@ class SyncProcessManager {
this._workers = {};
this._listenForSyncsClient = null;
this._exiting = false;
+ this._logger = global.Logger.child({identity: IDENTITY})
}
start() {
- console.log(`ProcessManager: Starting with ID ${IDENTITY}`)
+ this._logger.info(`ProcessManager: Starting with ID`)
this.unassignAccountsAssignedTo(IDENTITY).then(() => {
this.unassignAccountsMissingHeartbeats();
@@ -63,12 +64,14 @@ class SyncProcessManager {
client.setAsync(key, Date.now()).then(() =>
client.expireAsync(key, HEARTBEAT_EXPIRES)
).then(() =>
- console.log("ProcessManager: 💘")
+ this._logger.info({
+ accounts_syncing_count: Object.keys(this._workers).length,
+ }, "ProcessManager: 💘")
)
}
onSigInt() {
- console.log(`ProcessManager: Exiting...`)
+ this._logger.info(`ProcessManager: Exiting...`)
this._exiting = true;
this.unassignAccountsAssignedTo(IDENTITY).then(() =>
@@ -85,7 +88,7 @@ class SyncProcessManager {
let unseenIds = [].concat(accountIds);
- console.log("ProcessManager: Starting scan for accountIds in database that are not present in Redis.")
+ this._logger.info("ProcessManager: Starting scan for accountIds in database that are not present in Redis.")
return forEachAccountList((foundProcessIdentity, foundIds) => {
unseenIds = unseenIds.filter((a) => !foundIds.includes(`${a}`))
@@ -94,7 +97,10 @@ class SyncProcessManager {
if (unseenIds.length === 0) {
return;
}
- console.log(`ProcessManager: Adding account IDs ${unseenIds.join(',')} to ${ACCOUNTS_UNCLAIMED}.`)
+ this._logger.info({
+ unseen_ids: unseenIds.join(', '),
+ channel: ACCOUNTS_UNCLAIMED,
+ }, `ProcessManager: Adding unseen account IDs to ACCOUNTS_UNCLAIMED channel.`)
unseenIds.map((id) => client.lpushAsync(ACCOUNTS_UNCLAIMED, id));
});
}
@@ -102,7 +108,7 @@ class SyncProcessManager {
unassignAccountsMissingHeartbeats() {
const client = PubsubConnector.broadcastClient();
- console.log("ProcessManager: Starting unassignment for processes missing heartbeats.")
+ this._logger.info("ProcessManager: Starting unassignment for processes missing heartbeats.")
Promise.each(client.keysAsync(`${ACCOUNTS_CLAIMED_PREFIX}*`), (key) => {
const id = key.replace(ACCOUNTS_CLAIMED_PREFIX, '');
@@ -125,12 +131,15 @@ class SyncProcessManager {
)
return unassignOne(0).then((returned) => {
- console.log(`ProcessManager: Returned ${returned} accounts assigned to ${identity}.`)
+ this._logger.info({
+ returned,
+ assigned_to: identity,
+ }, `ProcessManager: Returned accounts`)
});
}
update() {
- console.log(`ProcessManager: Searching for an unclaimed account to sync.`)
+ this._logger.info(`ProcessManager: Searching for an unclaimed account to sync.`)
this.acceptUnclaimedAccount().finally(() => {
if (this._exiting) {
@@ -170,7 +179,7 @@ class SyncProcessManager {
if (this._exiting || this._workers[account.id]) {
return;
}
- console.log(`ProcessManager: Starting worker for Account ${accountId}`)
+ this._logger.info({account_id: accountId}, `ProcessManager: Starting worker for Account`)
this._workers[account.id] = new SyncWorker(account, db, () => {
this.removeWorkerForAccountId(accountId)
});
@@ -187,7 +196,8 @@ class SyncProcessManager {
if (didRemove) {
PubsubConnector.broadcastClient().rpushAsync(dst, accountId)
} else {
- throw new Error("Wanted to return item to pool, but didn't have claim on it.")
+ this._logger.error("Wanted to return item to pool, but didn't have claim on it.")
+ return
}
this._workers[accountId] = null;
});
diff --git a/packages/nylas-sync/sync-worker.js b/packages/nylas-sync/sync-worker.js
index 6ebb34b8d..7178e59cf 100644
--- a/packages/nylas-sync/sync-worker.js
+++ b/packages/nylas-sync/sync-worker.js
@@ -11,8 +11,8 @@ const {
const {CLAIM_DURATION} = SchedulerUtils;
-const FetchFolderList = require('./imap/fetch-category-list')
-const FetchMessagesInFolder = require('./imap/fetch-messages-in-category')
+const FetchFolderList = require('./imap/fetch-folder-list')
+const FetchMessagesInFolder = require('./imap/fetch-messages-in-folder')
const SyncbackTaskFactory = require('./syncback-task-factory')
@@ -24,6 +24,7 @@ class SyncWorker {
this._startTime = Date.now();
this._lastSyncTime = null;
this._onExpired = onExpired;
+ this._logger = global.Logger.forAccount(account)
this._syncTimer = null;
this._expirationTimer = null;
@@ -54,8 +55,11 @@ class SyncWorker {
this._onAccountUpdated(); break;
case MessageTypes.SYNCBACK_REQUESTED:
this.syncNow({reason: 'Syncback Action Queued'}); break;
+ case MessageTypes.ACCOUNT_CREATED:
+ // No other processing currently required for account creation
+ break;
default:
- throw new Error(`Invalid message: ${msg}`)
+ this._logger.error({message: msg}, 'SyncWorker: Invalid message')
}
}
@@ -63,10 +67,14 @@ class SyncWorker {
if (!this.isWaitingForNextSync()) {
return;
}
- this._getAccount().then((account) => {
+ this._getAccount()
+ .then((account) => {
this._account = account;
this.syncNow({reason: 'Account Modification'});
- });
+ })
+ .catch((err) => {
+ this._logger.error(err, 'SyncWorker: Error getting account for update')
+ })
}
_onConnectionIdleUpdate() {
@@ -100,7 +108,12 @@ class SyncWorker {
return Promise.reject(new Error("ensureConnection: There are no IMAP connection credentials for this account."))
}
- const conn = new IMAPConnection(this._db, Object.assign({}, settings, credentials));
+ const conn = new IMAPConnection({
+ db: this._db,
+ settings: Object.assign({}, settings, credentials),
+ logger: this._logger,
+ });
+
conn.on('mail', () => {
this._onConnectionIdleUpdate();
})
@@ -145,7 +158,7 @@ class SyncWorker {
)
return Promise.all(categoriesToSync.map((cat) =>
- this._conn.runOperation(new FetchMessagesInFolder(cat, folderSyncOptions))
+ this._conn.runOperation(new FetchMessagesInFolder(cat, folderSyncOptions, this._logger))
))
});
}
@@ -155,10 +168,10 @@ class SyncWorker {
this._syncTimer = null;
if (!process.env.SYNC_AFTER_ERRORS && this._account.errored()) {
- console.log(`SyncWorker: Account ${this._account.emailAddress} (${this._account.id}) is in error state - Skipping sync`)
+ this._logger.info(`SyncWorker: Account is in error state - Skipping sync`)
return
}
- console.log(`SyncWorker: Account ${this._account.emailAddress} (${this._account.id}) sync started (${reason})`)
+ this._logger.info({reason}, `SyncWorker: Account sync started`)
this.ensureConnection()
.then(() => this._account.update({syncError: null}))
@@ -174,7 +187,7 @@ class SyncWorker {
}
onSyncError(error) {
- console.error(`SyncWorker: Error while syncing account ${this._account.emailAddress} (${this._account.id})`, error)
+ this._logger.error(error, `SyncWorker: Error while syncing account`)
this.closeConnection()
if (error.source.includes('socket') || error.source.includes('timeout')) {
@@ -189,8 +202,8 @@ class SyncWorker {
onSyncDidComplete() {
const {afterSync} = this._account.syncPolicy;
- if (!this._account.firstSyncCompletedAt) {
- this._account.firstSyncCompletedAt = Date.now()
+ if (!this._account.firstSyncCompletion) {
+ this._account.firstSyncCompletion = Date.now()
}
const now = Date.now();
@@ -203,21 +216,22 @@ class SyncWorker {
this._account.lastSyncCompletions = lastSyncCompletions
this._account.save()
- console.log('Syncworker: Completed sync cycle')
+ this._logger.info('Syncworker: Completed sync cycle')
if (afterSync === 'idle') {
return this._getIdleFolder()
.then((idleFolder) => this._conn.openBox(idleFolder.name))
- .then(() => console.log('SyncWorker: - Idling on inbox category'))
+ .then(() => this._logger.info('SyncWorker: Idling on inbox category'))
}
if (afterSync === 'close') {
- console.log('SyncWorker: - Closing connection');
+ this._logger.info('SyncWorker: Closing connection');
this.closeConnection()
return Promise.resolve()
}
- throw new Error(`SyncWorker.onSyncDidComplete: Unknown afterSync behavior: ${afterSync}. Closing connection`)
+ this._logger.error({after_sync: afterSync}, `SyncWorker.onSyncDidComplete: Unknown afterSync behavior`)
+ throw new Error('SyncWorker.onSyncDidComplete: Unknown afterSync behavior')
}
isWaitingForNextSync() {
@@ -226,7 +240,7 @@ class SyncWorker {
scheduleNextSync() {
if (Date.now() - this._startTime > CLAIM_DURATION) {
- console.log("SyncWorker: - Has held account for more than CLAIM_DURATION, returning to pool.");
+ this._logger.info("SyncWorker: - Has held account for more than CLAIM_DURATION, returning to pool.");
this.cleanup();
this._onExpired();
return;
@@ -238,7 +252,10 @@ class SyncWorker {
if (interval) {
const target = this._lastSyncTime + interval;
- console.log(`SyncWorker: Account ${active ? 'active' : 'inactive'}. Next sync scheduled for ${new Date(target).toLocaleString()}`);
+ this._logger.info({
+ is_active: active,
+ next_sync: new Date(target).toLocaleString(),
+ }, `SyncWorker: Next sync scheduled`);
this._syncTimer = setTimeout(() => {
this.syncNow({reason: 'Scheduled'});
}, target - Date.now());
diff --git a/pm2-dev.yml b/pm2-dev.yml
index 313be4c0d..b8efea594 100644
--- a/pm2-dev.yml
+++ b/pm2-dev.yml
@@ -4,6 +4,7 @@ apps:
- script : packages/nylas-api/app.js
+ watch : ["packages"]
name : api
env :
PORT: 5100
@@ -12,25 +13,26 @@ apps:
GMAIL_CLIENT_ID : "271342407743-nibas08fua1itr1utq9qjladbkv3esdm.apps.googleusercontent.com"
GMAIL_CLIENT_SECRET : "WhmxErj-ei6vJXLocNhBbfBF"
GMAIL_REDIRECT_URL : "http://localhost:5100/auth/gmail/oauthcallback"
-
-
+ NODE_ENV: 'development'
- script : packages/nylas-sync/app.js
+ watch : ["packages"]
name : sync
env :
DB_ENCRYPTION_ALGORITHM : "aes-256-ctr"
DB_ENCRYPTION_PASSWORD : "d6F3Efeq"
-
-
+ NODE_ENV: 'development'
- script : packages/nylas-dashboard/app.js
+ watch : ["packages"]
name : dashboard
env :
PORT: 5101
DB_ENCRYPTION_ALGORITHM : "aes-256-ctr"
DB_ENCRYPTION_PASSWORD : "d6F3Efeq"
-
-
+ NODE_ENV: 'development'
- script : packages/nylas-message-processor/app.js
+ watch : ["packages"]
name : processor
env :
DB_ENCRYPTION_ALGORITHM : "aes-256-ctr"
DB_ENCRYPTION_PASSWORD : "d6F3Efeq"
+ NODE_ENV: 'development'