Repository: rickbergfalk/sqlpad
Pull request #547 (2020-02-21 17:26:47)

Wrap pino logger for app logging
Adds a `Logger` class and initializes an instance of it in `lib/appLog.js`. Instead of referencing config directly, `Logger` instances default to `warn` levels, and then can be set after initialization.

This separates the logger from config. 

The `Logger` class is also used as a reference for jsdoc hinting for the `appLog` added to the `req` object. 


Patch:
diff --git a/server/app.js b/server/app.js
index 0de4b94e..b54d7947 100644
--- a/server/app.js
+++ b/server/app.js
@@ -4,7 +4,7 @@ const express = require('express');
 const helmet = require('helmet');
 const session = require('express-session');
 const FileStore = require('session-file-store')(session);
-const logger = require('./lib/logger');
+const appLog = require('./lib/appLog');
 
 /**
  * Create an express app using config
@@ -64,6 +64,7 @@ function makeApp(config, models) {
   app.use(function(req, res, next) {
     req.config = config;
     req.models = models;
+    req.appLog = appLog;
     next();
   });
 
@@ -165,8 +166,8 @@ function makeApp(config, models) {
       .replace(/="\/static/g, `="${baseUrl}/static`);
     app.use((req, res) => res.send(baseUrlHtml));
   } else {
-    logger.warn('NO FRONT END TEMPLATE DETECTED');
-    logger.warn('If not running in dev mode please report this issue.');
+    appLog.warn('NO FRONT END TEMPLATE DETECTED');
+    appLog.warn('If not running in dev mode please report this issue.');
   }
 
   return app;
diff --git a/server/drivers/cassandra/index.js b/server/drivers/cassandra/index.js
index 66a494de..9ec76b80 100644
--- a/server/drivers/cassandra/index.js
+++ b/server/drivers/cassandra/index.js
@@ -1,5 +1,5 @@
 const cassandra = require('cassandra-driver');
-const logger = require('../../lib/logger');
+const appLog = require('../../lib/appLog');
 const { formatSchemaQueryResults } = require('../utils');
 
 const id = 'cassandra';
@@ -21,8 +21,8 @@ const SCHEMA_SQL = `
  */
 function shutdownClient(client) {
   client.shutdown().catch(error => {
-    logger.error('Error shutting down cassandra connection');
-    logger.error(error);
+    appLog.error('Error shutting down cassandra connection');
+    appLog.error(error);
   });
 }
 
diff --git a/server/drivers/drill/drill.js b/server/drivers/drill/drill.js
index 7071bd18..b28180ef 100644
--- a/server/drivers/drill/drill.js
+++ b/server/drivers/drill/drill.js
@@ -1,7 +1,7 @@
 const fetch = require('node-fetch');
 let request = require('request');
 let url = require('url');
-const logger = require('../../lib/logger');
+const appLog = require('../../lib/appLog');
 
 exports.version = '1.0';
 
@@ -72,7 +72,7 @@ Client.prototype.query = function(config, query) {
     })
     .catch(function(e) {
       // TODO Send error message to JSON
-      logger.error(e);
+      appLog.error(e);
       return e;
     });
 };
diff --git a/server/drivers/drill/index.js b/server/drivers/drill/index.js
index 2e0ebfab..1bc3c8df 100644
--- a/server/drivers/drill/index.js
+++ b/server/drivers/drill/index.js
@@ -1,5 +1,5 @@
 const drill = require('./drill.js');
-const logger = require('../../lib/logger');
+const appLog = require('../../lib/appLog');
 const { formatSchemaQueryResults } = require('../utils');
 
 const id = 'drill';
@@ -50,8 +50,8 @@ function runQuery(query, connection) {
     if (!result) {
       throw new Error('No result returned');
     } else if (result.errorMessage && result.errorMessage.length > 0) {
-      logger.info('Error with query: %s', query);
-      logger.info(result.errorMessage);
+      appLog.info('Error with query: %s', query);
+      appLog.info(result.errorMessage);
       throw new Error(result.errorMessage.split('\n')[0]);
     }
     if (result.length > connection.maxRows) {
diff --git a/server/drivers/hdb/index.js b/server/drivers/hdb/index.js
index 0e77f809..40605efb 100644
--- a/server/drivers/hdb/index.js
+++ b/server/drivers/hdb/index.js
@@ -1,5 +1,5 @@
 const hdb = require('hdb');
-const logger = require('../../lib/logger');
+const appLog = require('../../lib/appLog');
 const { formatSchemaQueryResults } = require('../utils');
 
 const id = 'hdb';
@@ -42,13 +42,13 @@ function runQuery(query, connection) {
     });
 
     client.on('error', err => {
-      logger.error(err, 'Network connection error');
+      appLog.error(err, 'Network connection error');
       return reject(err);
     });
 
     client.connect(err => {
       if (err) {
-        logger.error(err, 'Connect error');
+        appLog.error(err, 'Connect error');
         return reject(err);
       }
       return client.execute(query, function(err, rs) {
diff --git a/server/drivers/index.js b/server/drivers/index.js
index b8337470..19f2f3eb 100644
--- a/server/drivers/index.js
+++ b/server/drivers/index.js
@@ -2,7 +2,7 @@ const uuid = require('uuid');
 const _ = require('lodash');
 const utils = require('./utils');
 const getMeta = require('../lib/getMeta');
-const logger = require('../lib/logger');
+const appLog = require('../lib/appLog');
 
 const drivers = {};
 
@@ -14,7 +14,7 @@ const drivers = {};
  */
 function validateFunction(path, driver, functionName) {
   if (typeof driver[functionName] !== 'function') {
-    logger.error('%s missing .%s() implementation', path, functionName);
+    appLog.error('%s missing .%s() implementation', path, functionName);
     process.exit(1);
   }
 }
@@ -28,7 +28,7 @@ function validateFunction(path, driver, functionName) {
 function validateArray(path, driver, arrayName) {
   const arr = driver[arrayName];
   if (!Array.isArray(arr)) {
-    logger.error('%s missing %s array', path, arrayName);
+    appLog.error('%s missing %s array', path, arrayName);
     process.exit(1);
   }
 }
@@ -69,7 +69,7 @@ function requireValidate(path, optional = false) {
     driver = require(path);
   } catch (er) {
     if (optional) {
-      logger.info('optional driver %s not available', path);
+      appLog.info('optional driver %s not available', path);
       return;
     } else {
       // rethrow
@@ -78,18 +78,18 @@ function requireValidate(path, optional = false) {
   }
 
   if (!driver.id) {
-    logger.error('%s must export a unique id', path);
+    appLog.error('%s must export a unique id', path);
     process.exit(1);
   }
 
   if (!driver.name) {
-    logger.error('%s must export a name', path);
+    appLog.error('%s must export a name', path);
     process.exit(1);
   }
 
   if (drivers[driver.id]) {
-    logger.error(`Driver with id ${driver.id} already loaded`);
-    logger.error(`Ensure ${path} has a unique id exported`);
+    appLog.error(`Driver with id ${driver.id} already loaded`);
+    appLog.error(`Ensure ${path} has a unique id exported`);
     process.exit(1);
   }
 
@@ -163,7 +163,7 @@ function runQuery(query, connection, user) {
     const rowCount = rows.length;
     const { startTime, stopTime, queryRunTime } = queryResult;
 
-    logger.info({
+    appLog.info({
       userId: user && user._id,
       userEmail: user && user.email,
       connectionId: connection._id,
diff --git a/server/drivers/mysql/index.js b/server/drivers/mysql/index.js
index 781a79db..208f7c34 100644
--- a/server/drivers/mysql/index.js
+++ b/server/drivers/mysql/index.js
@@ -1,6 +1,6 @@
 const fs = require('fs');
 const mysql = require('mysql');
-const logger = require('../../lib/logger');
+const appLog = require('../../lib/appLog');
 const { formatSchemaQueryResults } = require('../utils');
 
 const id = 'mysql';
@@ -125,7 +125,7 @@ function runQuery(query, connection) {
             if (params.closeConnection) {
               myConnection.end(error => {
                 if (error) {
-                  logger.error(error, 'Error ending MySQL connection');
+                  appLog.error(error, 'Error ending MySQL connection');
                 }
                 continueOn();
               });
diff --git a/server/drivers/postgres/index.js b/server/drivers/postgres/index.js
index ada6ff2f..027c50b5 100644
--- a/server/drivers/postgres/index.js
+++ b/server/drivers/postgres/index.js
@@ -3,7 +3,7 @@ const pg = require('pg');
 const _ = require('lodash');
 const PgCursor = require('pg-cursor');
 const SocksConnection = require('socksjs');
-const logger = require('../../lib/logger');
+const appLog = require('../../lib/appLog');
 const { formatSchemaQueryResults } = require('../utils');
 
 const id = 'postgres';
@@ -136,13 +136,13 @@ function runQuery(query, connection) {
           if (params.closeConnection) {
             cursor.close(err => {
               if (err) {
-                logger.error(err, 'error closing pg-cursor');
+                appLog.error(err, 'error closing pg-cursor');
               }
               // Calling end() without setImmediate causes error within node-pg
               setImmediate(() => {
                 client.end(error => {
                   if (error) {
-                    logger.error(error);
+                    appLog.error(error);
                   }
                 });
               });
diff --git a/server/drivers/unixodbc/index.js b/server/drivers/unixodbc/index.js
index 5f8a7e00..ce647ac8 100644
--- a/server/drivers/unixodbc/index.js
+++ b/server/drivers/unixodbc/index.js
@@ -1,5 +1,5 @@
 const odbc = require('odbc');
-const logger = require('../../lib/logger');
+const appLog = require('../../lib/appLog');
 const { formatSchemaQueryResults } = require('../utils');
 
 const id = 'unixodbc';
@@ -81,7 +81,7 @@ async function runQuery(query, connection) {
 
     return { rows, incomplete };
   } catch (error) {
-    logger.error(error);
+    appLog.error(error);
     try {
       if (connectionInstance && connectionInstance.close) {
         await connectionInstance.close();
@@ -89,7 +89,7 @@ async function runQuery(query, connection) {
     } catch (error) {
       // Do nothing here.
       // An error already happened we're just trying to ensure it closed okay
-      logger.error(error, 'error closing connection after error');
+      appLog.error(error, 'error closing connection after error');
     }
     throw error;
   }
diff --git a/server/lib/appLog.js b/server/lib/appLog.js
new file mode 100644
index 00000000..7d767492
--- /dev/null
+++ b/server/lib/appLog.js
@@ -0,0 +1,4 @@
+const Logger = require('./logger');
+const appLog = new Logger();
+
+module.exports = appLog;
diff --git a/server/lib/cli-flow.js b/server/lib/cli-flow.js
index 31e25ae5..5ad3f356 100644
--- a/server/lib/cli-flow.js
+++ b/server/lib/cli-flow.js
@@ -1,6 +1,6 @@
 const minimist = require('minimist');
 const argv = minimist(process.argv.slice(2));
-const logger = require('./logger');
+const appLog = require('./appLog');
 const packageJson = require('../package.json');
 const configItems = require('./config/configItems');
 
@@ -27,12 +27,12 @@ ${configItems
 
 // If version is requested show version then exit
 if (argv.v || argv.version) {
-  logger.info('SQLPad version %s', packageJson.version);
+  appLog.info('SQLPad version %s', packageJson.version);
   process.exit();
 }
 
 // If help is requested show help
 if (argv.h || argv.help) {
-  logger.info(helpText);
+  appLog.info(helpText);
   process.exit();
 }
diff --git a/server/lib/connectionsFromConfig.js b/server/lib/connectionsFromConfig.js
index 44b7fdd6..cd38751d 100644
--- a/server/lib/connectionsFromConfig.js
+++ b/server/lib/connectionsFromConfig.js
@@ -1,5 +1,5 @@
 const _ = require('lodash');
-const logger = require('./logger');
+const appLog = require('./appLog');
 const drivers = require('../drivers');
 const getConfigFromFile = require('./config/fromFile.js');
 const [configFromFile] = getConfigFromFile() || {};
@@ -64,7 +64,7 @@ function getConnectionsFromConfig(env = process.env) {
       connection.editable = false;
       connectionsFromConfig.push(connection);
     } catch (error) {
-      logger.error(
+      appLog.error(
         error,
         'Environment connection configuration failed for %s',
         id
diff --git a/server/lib/db.js b/server/lib/db.js
index 7f52500f..d014c015 100644
--- a/server/lib/db.js
+++ b/server/lib/db.js
@@ -1,7 +1,7 @@
 const path = require('path');
 const datastore = require('nedb-promise');
 const mkdirp = require('mkdirp');
-const logger = require('./logger');
+const appLog = require('./appLog');
 const ensureAdmin = require('./ensureAdmin');
 const consts = require('./consts');
 const Models = require('../models');
@@ -70,14 +70,14 @@ async function initNedb(config) {
   // Load dbs, migrate data, and apply indexes
   await Promise.all(
     nedb.instances.map(dbname => {
-      logger.info('Loading %s', dbname);
+      appLog.info('Loading %s', dbname);
       return nedb[dbname].loadDatabase();
     })
   );
 
   // create default connection accesses
   if (allowConnectionAccessToEveryone) {
-    logger.info('Creating access on every connection to every user...');
+    appLog.info('Creating access on every connection to every user...');
     await nedb.connectionAccesses.update(
       {
         connectionId: consts.EVERY_CONNECTION_ID,
diff --git a/server/lib/email.js b/server/lib/email.js
index 66371acb..7611d8f1 100644
--- a/server/lib/email.js
+++ b/server/lib/email.js
@@ -1,5 +1,5 @@
 const nodemailer = require('nodemailer');
-const logger = require('./logger');
+const appLog = require('./appLog');
 
 function makeEmail(config) {
   /**
@@ -38,7 +38,7 @@ function makeEmail(config) {
 
   async function send(to, subject, text, html) {
     if (!config.smtpConfigured()) {
-      logger.error('email.send() called without being configured');
+      appLog.error('email.send() called without being configured');
       return;
     }
 
@@ -65,9 +65,9 @@ function makeEmail(config) {
         html
       };
       transporter.sendMail(mailOptions, function(err, info) {
-        logger.info('sent email: %s', info);
+        appLog.info('sent email: %s', info);
         if (err) {
-          logger.error(err);
+          appLog.error(err);
           return reject(err);
         }
         resolve(info);
diff --git a/server/lib/ensureAdmin.js b/server/lib/ensureAdmin.js
index a386700d..2190c2e6 100644
--- a/server/lib/ensureAdmin.js
+++ b/server/lib/ensureAdmin.js
@@ -1,5 +1,5 @@
 const passhash = require('./passhash');
-const logger = require('./logger');
+const appLog = require('./appLog');
 
 /**
  * Ensure admin email is a user if provided
@@ -25,7 +25,7 @@ async function ensureAdmin(nedb, adminEmail, adminPassword) {
         changes.passhash = await passhash.getPasshash(adminPassword);
       }
       await nedb.users.update({ _id: user._id }, { $set: changes }, {});
-      logger.info('Admin access granted to %s', adminEmail);
+      appLog.info('Admin access granted to %s', adminEmail);
       return;
     }
 
@@ -37,10 +37,10 @@ async function ensureAdmin(nedb, adminEmail, adminPassword) {
       newAdmin.passhash = await passhash.getPasshash(adminPassword);
     }
     await nedb.users.insert(newAdmin);
-    logger.info('Admin access granted to %s', adminEmail);
-    logger.info('Please visit signup to complete registration.');
+    appLog.info('Admin access granted to %s', adminEmail);
+    appLog.info('Please visit signup to complete registration.');
   } catch (error) {
-    logger.error('Admin access grant failed for %s', adminEmail);
+    appLog.error('Admin access grant failed for %s', adminEmail);
     throw error;
   }
 }
diff --git a/server/lib/logger.js b/server/lib/logger.js
index 00abe54c..cf7adba1 100644
--- a/server/lib/logger.js
+++ b/server/lib/logger.js
@@ -1,11 +1,49 @@
-const config = require('./config');
+const pino = require('pino');
 
 // Log levels https://github.com/pinojs/pino/issues/123
-const appLogLevel = config.get('appLogLevel');
+const levels = ['fatal', 'error', 'warn', 'info', 'debug', 'silent'];
 
-const logger = require('pino')({
-  name: 'sqlpad-app',
-  level: appLogLevel
-});
+class Logger {
+  constructor(logLevel = 'warn') {
+    if (!levels.includes(logLevel)) {
+      throw new Error(`Unknown log level ${logLevel}`);
+    }
+    this.logLevel = logLevel;
+    this.logger = pino({
+      name: 'sqlpad-app',
+      level: logLevel
+    });
+  }
 
-module.exports = logger;
+  setLevel(logLevel) {
+    if (!levels.includes(logLevel)) {
+      throw new Error(`Unknown log level ${logLevel}`);
+    }
+    this.logger = pino({
+      name: 'sqlpad-app',
+      level: logLevel
+    });
+  }
+
+  fatal(...args) {
+    this.logger.fatal(...args);
+  }
+
+  error(...args) {
+    this.logger.error(...args);
+  }
+
+  warn(...args) {
+    this.logger.warn(...args);
+  }
+
+  info(...args) {
+    this.logger.info(...args);
+  }
+
+  debug(...args) {
+    this.logger.debug(...args);
+  }
+}
+
+module.exports = Logger;
diff --git a/server/lib/pushQueryToSlack.js b/server/lib/pushQueryToSlack.js
index fd75662c..4a454140 100644
--- a/server/lib/pushQueryToSlack.js
+++ b/server/lib/pushQueryToSlack.js
@@ -1,4 +1,4 @@
-const logger = require('./logger');
+const appLog = require('./appLog');
 const request = require('request');
 
 function pushQueryToSlack(config, query) {
@@ -23,7 +23,7 @@ ${'```'}`
     };
     request(options, function(err) {
       if (err) {
-        logger.error(err, 'Problem sending query to slack');
+        appLog.error(err, 'Problem sending query to slack');
       }
     });
   }
diff --git a/server/lib/sendError.js b/server/lib/sendError.js
index aa30e6d9..63a4f515 100644
--- a/server/lib/sendError.js
+++ b/server/lib/sendError.js
@@ -1,4 +1,4 @@
-const logger = require('./logger');
+const appLog = require('./appLog');
 
 /**
  * Logs actual error and sends standard error response
@@ -8,7 +8,7 @@ const logger = require('./logger');
  */
 module.exports = function sendError(res, error, message) {
   if (error) {
-    logger.error(error);
+    appLog.error(error);
   }
   return res.json({
     error: message || (error ? error.toString() : 'Something happened')
diff --git a/server/models/resultCache.js b/server/models/resultCache.js
index 767da940..40b621d3 100644
--- a/server/models/resultCache.js
+++ b/server/models/resultCache.js
@@ -2,7 +2,7 @@ const fs = require('fs');
 const path = require('path');
 const moment = require('moment');
 const sanitize = require('sanitize-filename');
-const logger = require('../lib/logger');
+const appLog = require('../lib/appLog');
 const xlsx = require('node-xlsx');
 const { parse } = require('json2csv');
 
@@ -90,7 +90,7 @@ class ResultCache {
         await this.nedb.cache.remove({ _id: doc._id }, {});
       }
     } catch (error) {
-      logger.error(error);
+      appLog.error(error);
     }
   }
 
@@ -114,7 +114,7 @@ class ResultCache {
         // if there's an error log it but otherwise continue on
         // we can still send results even if download file failed to create
         if (err) {
-          logger.error(err);
+          appLog.error(err);
         }
         return resolve();
       });
@@ -127,12 +127,12 @@ class ResultCache {
         const csv = parse(queryResult.rows, { fields: queryResult.fields });
         fs.writeFile(this.csvFilePath(cacheKey), csv, function(err) {
           if (err) {
-            logger.error(err);
+            appLog.error(err);
           }
           return resolve();
         });
       } catch (error) {
-        logger.error(error);
+        appLog.error(error);
         return resolve();
       }
     });
@@ -144,12 +144,12 @@ class ResultCache {
         const json = JSON.stringify(queryResult.rows);
         fs.writeFile(this.jsonFilePath(cacheKey), json, function(err) {
           if (err) {
-            logger.error(err);
+            appLog.error(err);
           }
           return resolve();
         });
       } catch (error) {
-        logger.error(error);
+        appLog.error(error);
         return resolve();
       }
     });
diff --git a/server/routes/connection-accesses.js b/server/routes/connection-accesses.js
index a10b9edd..5aeb74ae 100644
--- a/server/routes/connection-accesses.js
+++ b/server/routes/connection-accesses.js
@@ -6,7 +6,7 @@ const mustBeAuthenticated = require('../middleware/must-be-authenticated.js');
 const sendError = require('../lib/sendError');
 
 /**
- * @param {Req} req
+ * @param {import('express').Request & Req} req
  * @param {*} res
  */
 async function listConnectionAccesses(req, res) {
@@ -32,7 +32,7 @@ router.get(
 );
 
 /**
- * @param {Req} req
+ * @param {import('express').Request & Req} req
  * @param {*} res
  */
 async function getConnectionAccess(req, res) {
@@ -57,7 +57,7 @@ router.get(
 );
 
 /**
- * @param {Req} req
+ * @param {import('express').Request & Req} req
  * @param {*} res
  */
 async function createConnectionAccess(req, res) {
@@ -118,7 +118,7 @@ async function createConnectionAccess(req, res) {
 router.post('/api/connection-accesses', mustBeAdmin, createConnectionAccess);
 
 /**
- * @param {Req} req
+ * @param {import('express').Request & Req} req
  * @param {*} res
  */
 async function updateConnectionAccess(req, res) {
diff --git a/server/routes/download-results.js b/server/routes/download-results.js
index 4fa5097a..b4efdab9 100644
--- a/server/routes/download-results.js
+++ b/server/routes/download-results.js
@@ -1,9 +1,8 @@
 const fs = require('fs');
 const router = require('express').Router();
-const logger = require('../lib/logger');
 
 router.get('/download-results/:cacheKey.csv', async function(req, res, next) {
-  const { models } = req;
+  const { models, appLog } = req;
   const { cacheKey } = req.params;
   try {
     if (req.config.get('allowCsvDownload')) {
@@ -22,14 +21,14 @@ router.get('/download-results/:cacheKey.csv', async function(req, res, next) {
       return next(new Error('CSV download disabled'));
     }
   } catch (error) {
-    logger.error(error);
+    appLog.error(error);
     // TODO figure out what this sends and set manually
     return next(error);
   }
 });
 
 router.get('/download-results/:cacheKey.xlsx', async function(req, res, next) {
-  const { models } = req;
+  const { models, appLog } = req;
   const { cacheKey } = req.params;
   try {
     if (req.config.get('allowCsvDownload')) {
@@ -51,14 +50,14 @@ router.get('/download-results/:cacheKey.xlsx', async function(req, res, next) {
       return next(new Error('XLSX download disabled'));
     }
   } catch (error) {
-    logger.error(error);
+    appLog.error(error);
     // TODO figure out what this sends and set manually
     return next(error);
   }
 });
 
 router.get('/download-results/:cacheKey.json', async function(req, res, next) {
-  const { models } = req;
+  const { models, appLog } = req;
   const { cacheKey } = req.params;
   try {
     if (req.config.get('allowCsvDownload')) {
@@ -77,7 +76,7 @@ router.get('/download-results/:cacheKey.json', async function(req, res, next) {
       return next(new Error('JSON download disabled'));
     }
   } catch (error) {
-    logger.error(error);
+    appLog.error(error);
     // TODO figure out what this sends and set manually
     return next(error);
   }
diff --git a/server/routes/forgot-password.js b/server/routes/forgot-password.js
index 66213450..5d6cb260 100644
--- a/server/routes/forgot-password.js
+++ b/server/routes/forgot-password.js
@@ -2,10 +2,9 @@ const router = require('express').Router();
 const uuid = require('uuid');
 const makeEmail = require('../lib/email');
 const sendError = require('../lib/sendError');
-const logger = require('../lib/logger');
 
 router.post('/api/forgot-password', async function(req, res) {
-  const { models } = req;
+  const { models, appLog } = req;
   if (!req.body.email) {
     return sendError(res, null, 'Email address must be provided');
   }
@@ -32,7 +31,7 @@ router.post('/api/forgot-password', async function(req, res) {
     const resetPath = `/password-reset/${user.passwordResetId}`;
     email
       .sendForgotPassword(req.body.email, resetPath)
-      .catch(error => logger.error(error));
+      .catch(error => appLog.error(error));
 
     return res.json({});
   } catch (error) {
diff --git a/server/routes/local-auth.js b/server/routes/local-auth.js
index 032f7a79..8558db39 100644
--- a/server/routes/local-auth.js
+++ b/server/routes/local-auth.js
@@ -4,7 +4,7 @@ const BasicStrategy = require('passport-http').BasicStrategy;
 const router = require('express').Router();
 const checkWhitelist = require('../lib/check-whitelist');
 const sendError = require('../lib/sendError');
-const logger = require('../lib/logger');
+const appLog = require('../lib/appLog');
 const passhash = require('../lib/passhash.js');
 
 async function handleSignup(req, res, next) {
@@ -66,7 +66,7 @@ function makeLocalAuth(config) {
     // Register local auth strategy
     // TODO: Should all authentication strategies be disabled by default, requiring SQLPad implementer to pick one?
     // This might be more secure as it'd prevent SQLPad's from launching with local auth enabled and open admin registration by default
-    logger.info('Enabling local authentication strategy.');
+    appLog.info('Enabling local authentication strategy.');
     passport.use(
       new PassportLocalStrategy(
         {
@@ -106,7 +106,7 @@ function makeLocalAuth(config) {
     );
 
     // TODO - basic auth should perhaps be something opted into as a separate config
-    logger.info('Enabling basic authentication strategy.');
+    appLog.info('Enabling basic authentication strategy.');
     passport.use(
       new BasicStrategy(
         {
diff --git a/server/routes/oauth.js b/server/routes/oauth.js
index 60ab112b..b56baa9d 100644
--- a/server/routes/oauth.js
+++ b/server/routes/oauth.js
@@ -1,7 +1,7 @@
 const passport = require('passport');
 const PassportGoogleStrategy = require('passport-google-oauth20').Strategy;
 const router = require('express').Router();
-const logger = require('../lib/logger');
+const appLog = require('../lib/appLog');
 const checkWhitelist = require('../lib/check-whitelist.js');
 
 /**
@@ -64,7 +64,7 @@ function makeGoogleAuth(config) {
   }
 
   if (config.googleAuthConfigured()) {
-    logger.info('Enabling Google authentication strategy.');
+    appLog.info('Enabling Google authentication strategy.');
 
     // Register the passport strategy
     passport.use(
diff --git a/server/routes/saml.js b/server/routes/saml.js
index 0af74297..b7f18d6e 100644
--- a/server/routes/saml.js
+++ b/server/routes/saml.js
@@ -1,7 +1,7 @@
 const passport = require('passport');
 const SamlStrategy = require('passport-saml').Strategy;
 const router = require('express').Router();
-const logger = require('../lib/logger');
+const appLog = require('../lib/appLog');
 
 /**
  * Adds passport SAML strategy and SAML auth routes if SAML is configured
@@ -21,7 +21,7 @@ function makeSamlAuth(config) {
     samlCert &&
     samlAuthContext
   ) {
-    logger.info('Enabling SAML authentication strategy.');
+    appLog.info('Enabling SAML authentication strategy.');
 
     // Register SAML strategy
     passport.use(
@@ -43,7 +43,7 @@ function makeSamlAuth(config) {
             ];
           const { models } = req;
           const user = await models.users.findOneByEmail(email);
-          logger.info('User logged in via SAML %s', email);
+          appLog.info('User logged in via SAML %s', email);
           if (!user) {
             return done(null, false);
           }
diff --git a/server/routes/signout.js b/server/routes/signout.js
index 921160ac..d0edc738 100644
--- a/server/routes/signout.js
+++ b/server/routes/signout.js
@@ -1,5 +1,5 @@
 const router = require('express').Router();
-const logger = require('../lib/logger');
+const appLog = require('../lib/appLog');
 
 // Regardless of authentication strategy, signout route should always exist
 // It clears out the session which is used regardless of strategy
@@ -9,7 +9,7 @@ router.get('/api/signout', function(req, res) {
   }
   req.session.destroy(function(err) {
     if (err) {
-      logger.error(err);
+      appLog.error(err);
     }
     res.json({});
   });
diff --git a/server/routes/users.js b/server/routes/users.js
index c63c7969..450bb334 100644
--- a/server/routes/users.js
+++ b/server/routes/users.js
@@ -3,7 +3,6 @@ const makeEmail = require('../lib/email');
 const mustBeAdmin = require('../middleware/must-be-admin.js');
 const mustBeAuthenticated = require('../middleware/must-be-authenticated.js');
 const sendError = require('../lib/sendError');
-const logger = require('../lib/logger');
 
 router.get('/api/users', mustBeAuthenticated, async function(req, res) {
   const { models } = req;
@@ -17,7 +16,7 @@ router.get('/api/users', mustBeAuthenticated, async function(req, res) {
 
 // create/whitelist/invite user
 router.post('/api/users', mustBeAdmin, async function(req, res) {
-  const { models } = req;
+  const { models, appLog } = req;
   try {
     let user = await models.users.findOneByEmail(req.body.email);
     if (user) {
@@ -31,7 +30,7 @@ router.post('/api/users', mustBeAdmin, async function(req, res) {
     const email = makeEmail(req.config);
 
     if (req.config.smtpConfigured()) {
-      email.sendInvite(req.body.email).catch(error => logger.error(error));
+      email.sendInvite(req.body.email).catch(error => appLog.error(error));
     }
     return res.json({ user });
   } catch (error) {
diff --git a/server/server.js b/server/server.js
index cd5f1e0c..a5b05957 100755
--- a/server/server.js
+++ b/server/server.js
@@ -9,16 +9,18 @@ const makeApp = require('./app');
 // Parse command line flags to see if anything special needs to happen
 require('./lib/cli-flow.js');
 
-const logger = require('./lib/logger');
+const appLog = require('./lib/appLog');
 const config = require('./lib/config');
 const { makeDb, getDb } = require('./lib/db');
 
+appLog.setLevel(config.get('appLogLevel'));
+
 makeDb(config);
 
 const configValidations = config.getValidations();
-configValidations.warnings.map(warning => logger.warn(warning));
+configValidations.warnings.map(warning => appLog.warn(warning));
 if (configValidations.errors.length > 0) {
-  configValidations.errors.forEach(error => logger.error(error));
+  configValidations.errors.forEach(error => appLog.error(error));
   process.exit(1);
 }
 
@@ -53,10 +55,10 @@ function detectPortOrSystemd(port) {
     // We just crab the first socket from fd 3 since sqlpad binds only one
     // port.
     if (passedSocketCount > 0) {
-      logger.info('Using port from Systemd');
+      appLog.info('Using port from Systemd');
       return Promise.resolve({ fd: 3 });
     } else {
-      logger.warn(
+      appLog.warn(
         'Warning: Systemd socket asked but not found. Trying to bind port %d manually',
         port
       );
@@ -78,7 +80,7 @@ async function startServer(models) {
     // https only
     const _port = await detectPortOrSystemd(httpsPort);
     if (!isFdObject(_port) && httpsPort !== _port) {
-      logger.info(
+      appLog.info(
         'Port %d already occupied. Using port %d instead.',
         httpsPort,
         _port
@@ -100,13 +102,13 @@ async function startServer(models) {
       .listen(_port, ip, function() {
         const hostIp = ip === '0.0.0.0' ? 'localhost' : ip;
         const url = `https://${hostIp}:${_port}${baseUrl}`;
-        logger.info('Welcome to SQLPad!. Visit %s to get started', url);
+        appLog.info('Welcome to SQLPad!. Visit %s to get started', url);
       });
   } else {
     // http only
     const _port = await detectPortOrSystemd(port);
     if (!isFdObject(_port) && port !== _port) {
-      logger.info(
+      appLog.info(
         'Port %d already occupied. Using port %d instead.',
         port,
         _port
@@ -118,7 +120,7 @@ async function startServer(models) {
     server = http.createServer(app).listen(_port, ip, function() {
       const hostIp = ip === '0.0.0.0' ? 'localhost' : ip;
       const url = `http://${hostIp}:${_port}${baseUrl}`;
-      logger.info('Welcome to SQLPad! Visit %s to get started', url);
+      appLog.info('Welcome to SQLPad! Visit %s to get started', url);
     });
   }
   server.setTimeout(timeoutSeconds * 1000);
@@ -127,16 +129,16 @@ async function startServer(models) {
 getDb()
   .then(db => startServer(db.models))
   .catch(error => {
-    logger.error(error, 'Error starting SQLPad');
+    appLog.error(error, 'Error starting SQLPad');
     process.exit(1);
   });
 
 function handleShutdownSignal(signal) {
   if (!server) {
-    logger.info('Received %s, but no server to shutdown', signal);
+    appLog.info('Received %s, but no server to shutdown', signal);
     process.exit(0);
   } else {
-    logger.info('Received %s, shutting down server...', signal);
+    appLog.info('Received %s, shutting down server...', signal);
     server.close(function() {
       process.exit(0);
     });
diff --git a/server/typedefs.js b/server/typedefs.js
index f3dcab33..ab0b33bc 100644
--- a/server/typedefs.js
+++ b/server/typedefs.js
@@ -1,5 +1,8 @@
 /**
  * The expressjs Request object plus decorations
  * @typedef {Object} Req
+ * @property {Object} config
+ * @property {Object} log - Pino web logger. Same as appLog but no setLevel
+ * @property {import('./lib/logger')} appLog - Pino app logger
  * @property {import('./models')} models - Collection of data access objects
  */


Test patch:
diff --git a/server/test/utils.js b/server/test/utils.js
index 30b62a3c..800026c4 100644
--- a/server/test/utils.js
+++ b/server/test/utils.js
@@ -2,6 +2,7 @@ const assert = require('assert');
 const request = require('supertest');
 const consts = require('../lib/consts');
 const config = require('../lib/config');
+const appLog = require('../lib/appLog');
 const { makeDb, getDb } = require('../lib/db');
 const makeApp = require('../app');
 
@@ -96,6 +97,7 @@ async function put(role, url, body, statusCode = 200) {
 
 before(async function() {
   const { models } = await getDb();
+  appLog.setLevel(config.get('appLogLevel'));
   app = makeApp(config, models);
 
   assert.throws(() => {