@villedemontreal/logger
Version:
Logger and logging utilities
675 lines (573 loc) • 22.7 kB
text/typescript
import { utils } from '@villedemontreal/general-utils';
import { assert } from 'chai';
import * as fs from 'fs-extra';
import * as _ from 'lodash';
import { LoggerConfigs } from './config/configs';
import { constants } from './config/constants';
import { LazyLogger } from './lazyLogger';
import {
convertLogLevelToPinoNumberLevel,
createLogger,
ILogger,
initLogger,
Logger,
LogLevel,
setGlobalLogLevel,
} from './logger';
const TESTING_CID = 'test-cid';
/**
* The "--no-timeouts" arg doesn't work to disable
* the Mocha timeouts (while debugging) if a timeout
* is specified in the code itself. Using this to set the
* timeouts does.
*/
export function timeout(mocha: Mocha.Suite | Mocha.Context, milliSec: number) {
mocha.timeout(process.argv.includes('--no-timeouts') ? 0 : milliSec);
}
// ==========================================
// Logger tests
// ==========================================
describe('Logger tests', () => {
describe('General tests', () => {
let loggerConfig: LoggerConfigs;
let logger: ILogger;
const stdoutWriteBackup = process.stdout.write;
let output: string;
before(async () => {
// ==========================================
// Tweaks the configs for this tests file.
// ==========================================
loggerConfig = new LoggerConfigs(() => TESTING_CID);
loggerConfig.setLogHumanReadableinConsole(false);
loggerConfig.setLogSource(true);
loggerConfig.setLogLevel(LogLevel.INFO);
// ==========================================
// Creates the logger
// ==========================================
initLogger(loggerConfig, 'default', true);
logger = new Logger('test');
});
let expectTwoCalls = false;
let keepSecondCallOnly = false;
let firstCallDone = false;
function restartCustomWriter() {
output = '';
expectTwoCalls = false;
keepSecondCallOnly = false;
firstCallDone = false;
// A stadard "function" is required here, because
// of the use of "arguments".
// tslint:disable-next-line:only-arrow-functions
process.stdout.write = function (...args: string[]) {
if (!expectTwoCalls) {
output += args[0];
process.stdout.write = stdoutWriteBackup;
return;
}
if (!firstCallDone) {
if (!keepSecondCallOnly) {
output += args[0];
}
firstCallDone = true;
} else {
output += args[0];
process.stdout.write = stdoutWriteBackup;
}
} as any;
assert.isTrue(utils.isBlank(output));
}
beforeEach(async () => {
restartCustomWriter();
});
it('string message', async () => {
logger.error('allo');
assert.isTrue(!utils.isBlank(output));
const jsonObj = JSON.parse(output);
assert.isTrue(_.isObject(jsonObj));
assert.strictEqual(jsonObj.name, 'test');
assert.strictEqual(jsonObj.msg, 'allo');
assert.isNotNull(jsonObj.src);
assert.isNotNull(jsonObj.src.file);
assert.isNotNull(jsonObj.src.line);
assert.strictEqual(jsonObj.logType, constants.logging.logType.MONTREAL);
assert.strictEqual(jsonObj.logTypeVersion, '2');
});
it('string message and extra text message', async () => {
logger.error('allo', 'salut');
assert.isTrue(!utils.isBlank(output));
const jsonObj = JSON.parse(output);
assert.isTrue(_.isObject(jsonObj));
assert.strictEqual(jsonObj.msg, 'allo - salut');
assert.isNotNull(jsonObj.src);
assert.isNotNull(jsonObj.src.file);
assert.isNotNull(jsonObj.src.line);
assert.strictEqual(jsonObj.logType, constants.logging.logType.MONTREAL);
assert.strictEqual(jsonObj.logTypeVersion, '2');
});
it('custom object message', async () => {
logger.error({
key1: {
key3: 'val3',
key4: 'val4',
},
key2: 'val2',
msg: 'blabla',
});
assert.isTrue(!utils.isBlank(output));
const jsonObj = JSON.parse(output);
assert.strictEqual(jsonObj.key2, 'val2');
assert.strictEqual(jsonObj.msg, 'blabla');
assert.deepEqual(jsonObj.key1, {
key3: 'val3',
key4: 'val4',
});
assert.isNotNull(jsonObj.src);
assert.isNotNull(jsonObj.src.file);
assert.isNotNull(jsonObj.src.line);
assert.strictEqual(jsonObj.logType, constants.logging.logType.MONTREAL);
assert.strictEqual(jsonObj.logTypeVersion, '2');
});
it('custom object message and extra text message', async () => {
logger.error(
{
key1: {
key3: 'val3',
key4: 'val4',
},
key2: 'val2',
msg: 'blabla',
},
'my text message',
);
assert.isTrue(!utils.isBlank(output));
const jsonObj = JSON.parse(output);
assert.strictEqual(jsonObj.key2, 'val2');
assert.strictEqual(jsonObj.msg, 'blabla - my text message');
assert.deepEqual(jsonObj.key1, {
key3: 'val3',
key4: 'val4',
});
assert.isNotNull(jsonObj.src);
assert.isNotNull(jsonObj.src.file);
assert.isNotNull(jsonObj.src.line);
assert.strictEqual(jsonObj.logType, constants.logging.logType.MONTREAL);
assert.strictEqual(jsonObj.logTypeVersion, '2');
});
it('regular Error object and extra text message', async () => {
logger.error(new Error('my error message'), 'my text message');
assert.isTrue(!utils.isBlank(output));
const jsonObj = JSON.parse(output);
assert.strictEqual(jsonObj.msg, 'my error message - my text message');
assert.isTrue(!utils.isBlank(jsonObj.stack));
assert.isTrue(!utils.isBlank(jsonObj.name));
assert.isNotNull(jsonObj.src);
assert.isNotNull(jsonObj.src.file);
assert.isNotNull(jsonObj.src.line);
assert.strictEqual(jsonObj.logType, constants.logging.logType.MONTREAL);
assert.strictEqual(jsonObj.logTypeVersion, '2');
});
it('null Error object and extra text message', async () => {
logger.error(null, 'my text message');
assert.isTrue(!utils.isBlank(output));
const jsonObj = JSON.parse(output);
assert.strictEqual(jsonObj.msg, 'my text message');
assert.isTrue(!utils.isBlank(jsonObj.name));
assert.isNotNull(jsonObj.src);
assert.isNotNull(jsonObj.src.file);
assert.isNotNull(jsonObj.src.line);
assert.strictEqual(jsonObj.logType, constants.logging.logType.MONTREAL);
assert.strictEqual(jsonObj.logTypeVersion, '2');
});
it('undefined Error object and extra text message', async () => {
logger.error(undefined, 'my text message');
assert.isTrue(!utils.isBlank(output));
const jsonObj = JSON.parse(output);
assert.strictEqual(jsonObj.msg, 'my text message');
assert.isTrue(!utils.isBlank(jsonObj.name));
assert.isNotNull(jsonObj.src);
assert.isNotNull(jsonObj.src.file);
assert.isNotNull(jsonObj.src.line);
assert.strictEqual(jsonObj.logType, constants.logging.logType.MONTREAL);
assert.strictEqual(jsonObj.logTypeVersion, '2');
});
it('array message', async () => {
// ==========================================
// Two calls to "process.stdout.write" will
// be made because an error for the invalid
// array message will be logged!
// ==========================================
expectTwoCalls = true;
keepSecondCallOnly = true;
logger.error([
'toto',
{
key1: 'val1',
key2: 'val2',
},
]);
const jsonObj = JSON.parse(output);
assert.deepEqual(jsonObj._arrayMsg, [
'toto',
{
key1: 'val1',
key2: 'val2',
},
]);
assert.isNotNull(jsonObj.src);
assert.isNotNull(jsonObj.src.file);
assert.isNotNull(jsonObj.src.line);
});
it('array message and extra text message', async () => {
// ==========================================
// Two calls to "process.stdout.write" will
// be made because an error for the invalid
// array message will be logged!
// ==========================================
expectTwoCalls = true;
keepSecondCallOnly = true;
logger.error(
[
'toto',
{
key1: 'val1',
key2: 'val2',
},
],
'my text message',
);
const jsonObj = JSON.parse(output);
assert.deepEqual(jsonObj._arrayMsg, [
'toto',
{
key1: 'val1',
key2: 'val2',
},
]);
assert.strictEqual(jsonObj.msg, 'my text message');
assert.isNotNull(jsonObj.src);
assert.isNotNull(jsonObj.src.file);
assert.isNotNull(jsonObj.src.line);
});
it('log level - debug', async () => {
logger.debug('allo');
assert.strictEqual(output, '');
});
it('log level - info', async () => {
logger.info('allo');
const jsonObj = JSON.parse(output);
assert.strictEqual(jsonObj.level, convertLogLevelToPinoNumberLevel(LogLevel.INFO));
});
it('log level - warning', async () => {
logger.warning('allo');
const jsonObj = JSON.parse(output);
assert.strictEqual(jsonObj.level, convertLogLevelToPinoNumberLevel(LogLevel.WARNING));
});
it('log level - error', async () => {
logger.error('allo');
const jsonObj = JSON.parse(output);
assert.strictEqual(jsonObj.level, convertLogLevelToPinoNumberLevel(LogLevel.ERROR));
});
it('log level - custom valid', async () => {
logger.log(LogLevel.INFO, 'allo');
const jsonObj = JSON.parse(output);
assert.strictEqual(jsonObj.level, convertLogLevelToPinoNumberLevel(LogLevel.INFO));
});
it('log level - custom invalid', async () => {
// ==========================================
// Two calls to "process.stdout.write" will
// be made because an error for the invalid
// level will be logged!
// ==========================================
expectTwoCalls = true;
keepSecondCallOnly = true;
logger.log('nope' as any, 'allo');
const jsonObj = JSON.parse(output);
assert.strictEqual(jsonObj.level, convertLogLevelToPinoNumberLevel(LogLevel.ERROR));
assert.strictEqual(jsonObj.msg, 'allo');
});
it('newline after each log - string', async () => {
expectTwoCalls = true;
logger.error('111');
logger.error('222');
const pos = output.indexOf('\n');
assert.isTrue(pos > -1);
assert.strictEqual(output.charAt(pos + 1), '{');
});
it('newline after each log - complexe objects', async () => {
expectTwoCalls = true;
logger.error({
key1: 'val1',
key2: 'val2',
});
logger.error({
key1: 'val1',
key2: 'val2',
});
const pos = output.indexOf('\n');
assert.isTrue(pos > -1);
assert.strictEqual(output.charAt(pos + 1), '{');
});
it('date message', async () => {
const someDate = new Date();
logger.error(someDate);
assert.isTrue(!utils.isBlank(output));
const jsonObj = JSON.parse(output);
assert.isTrue(_.isObject(jsonObj));
assert.strictEqual(jsonObj.msg, someDate.toString());
assert.isTrue(_.isDate(someDate));
});
// ==========================================
// Error controller log messages
// ==========================================
describe('Error controller log messages', () => {
it('app and version properties', async () => {
const packagePath = `${constants.libRoot}/../../package.json`;
const packageJson = require(packagePath);
const appName = packageJson.name;
const appVersion = packageJson.version;
logger.error('allo');
assert.isTrue(!utils.isBlank(output));
const jsonObj = JSON.parse(output);
assert.isTrue(_.isObject(jsonObj));
assert.strictEqual(jsonObj[constants.logging.properties.APP_NAME], appName);
assert.strictEqual(jsonObj[constants.logging.properties.APP_VERSION], appVersion);
assert.strictEqual(jsonObj[constants.logging.properties.CORRELATION_ID], TESTING_CID);
});
});
// ==========================================
// LazyLogger tests
// ==========================================
describe('LazyLogger tests', () => {
let lazyLogger: LazyLogger;
beforeEach(async () => {
lazyLogger = new LazyLogger('titi', (name: string): ILogger => {
const logger2 = new Logger('titi');
return logger2;
});
});
it('debug', async () => {
lazyLogger.debug('allo');
assert.isTrue(utils.isBlank(output));
});
it('info', async () => {
lazyLogger.info('allo');
assert.isTrue(!utils.isBlank(output));
const jsonObj = JSON.parse(output);
assert.isTrue(_.isObject(jsonObj));
assert.strictEqual(jsonObj.level, convertLogLevelToPinoNumberLevel(LogLevel.INFO));
assert.strictEqual(jsonObj.name, 'titi');
assert.strictEqual(jsonObj.msg, 'allo');
assert.isNotNull(jsonObj.src);
assert.isNotNull(jsonObj.src.file);
assert.isNotNull(jsonObj.src.line);
assert.strictEqual(jsonObj.logType, constants.logging.logType.MONTREAL);
assert.strictEqual(jsonObj.logTypeVersion, '2');
assert.strictEqual(jsonObj[constants.logging.properties.CORRELATION_ID], TESTING_CID);
});
it('warning', async () => {
lazyLogger.warning('allo');
assert.isTrue(!utils.isBlank(output));
const jsonObj = JSON.parse(output);
assert.isTrue(_.isObject(jsonObj));
assert.strictEqual(jsonObj.level, convertLogLevelToPinoNumberLevel(LogLevel.WARNING));
assert.strictEqual(jsonObj.name, 'titi');
assert.strictEqual(jsonObj.msg, 'allo');
assert.isNotNull(jsonObj.src);
assert.isNotNull(jsonObj.src.file);
assert.isNotNull(jsonObj.src.line);
assert.strictEqual(jsonObj.logType, constants.logging.logType.MONTREAL);
assert.strictEqual(jsonObj.logTypeVersion, '2');
assert.strictEqual(jsonObj[constants.logging.properties.CORRELATION_ID], TESTING_CID);
});
it('error', async () => {
lazyLogger.error('allo');
assert.isTrue(!utils.isBlank(output));
const jsonObj = JSON.parse(output);
assert.isTrue(_.isObject(jsonObj));
assert.strictEqual(jsonObj.level, convertLogLevelToPinoNumberLevel(LogLevel.ERROR));
assert.strictEqual(jsonObj.name, 'titi');
assert.strictEqual(jsonObj.msg, 'allo');
assert.isNotNull(jsonObj.src);
assert.isNotNull(jsonObj.src.file);
assert.isNotNull(jsonObj.src.line);
assert.strictEqual(jsonObj.logType, constants.logging.logType.MONTREAL);
assert.strictEqual(jsonObj.logTypeVersion, '2');
assert.strictEqual(jsonObj[constants.logging.properties.CORRELATION_ID], TESTING_CID);
});
it('log', async () => {
lazyLogger.log(LogLevel.INFO, 'allo');
assert.isTrue(!utils.isBlank(output));
const jsonObj = JSON.parse(output);
assert.isTrue(_.isObject(jsonObj));
assert.strictEqual(jsonObj.level, convertLogLevelToPinoNumberLevel(LogLevel.INFO));
assert.strictEqual(jsonObj.name, 'titi');
assert.strictEqual(jsonObj.msg, 'allo');
assert.isNotNull(jsonObj.src);
assert.isNotNull(jsonObj.src.file);
assert.isNotNull(jsonObj.src.line);
assert.strictEqual(jsonObj.logType, constants.logging.logType.MONTREAL);
assert.strictEqual(jsonObj.logTypeVersion, '2');
assert.strictEqual(jsonObj[constants.logging.properties.CORRELATION_ID], TESTING_CID);
});
it('logger creator is required', async () => {
try {
const lazyLogger2 = new LazyLogger('titi', null);
assert.fail();
assert.isNotOk(lazyLogger2);
} catch (err) {
/* ok */
}
try {
const lazyLogger3 = new LazyLogger('titi', undefined);
assert.fail();
assert.isNotOk(lazyLogger3);
} catch (err) {
/* ok */
}
});
});
// ==========================================
// Global log level
// ==========================================
describe('Global log level', () => {
let childLogger: ILogger;
before(() => {
childLogger = createLogger('testing child logger');
});
afterEach(() => {
// Reset the default logging
setGlobalLogLevel(loggerConfig.getLogLevel());
});
it('log debug message when global log level set to DEBUG', () => {
setGlobalLogLevel(LogLevel.DEBUG);
logger.debug('this is the debug message');
let jsonObj = JSON.parse(output);
assert.strictEqual(jsonObj.msg, 'this is the debug message');
restartCustomWriter();
childLogger.debug('this is the child logger debug message');
jsonObj = JSON.parse(output);
assert.strictEqual(jsonObj.msg, 'this is the child logger debug message');
restartCustomWriter();
logger.info('this is the info message');
jsonObj = JSON.parse(output);
assert.strictEqual(jsonObj.msg, 'this is the info message');
restartCustomWriter();
childLogger.info('this is the child logger info message');
jsonObj = JSON.parse(output);
assert.strictEqual(jsonObj.msg, 'this is the child logger info message');
});
it('filter debug message when global log level set to WARNING', () => {
assert.isTrue(utils.isBlank(output));
setGlobalLogLevel(LogLevel.WARNING);
logger.debug('this is my filtered the debug message');
assert.isTrue(utils.isBlank(output), 'Did not filter debug message as expected.');
childLogger.debug('this is my filtered the debug message');
assert.isTrue(utils.isBlank(output), 'Did not filter debug message as expected.');
logger.info('this is my filtered the info message');
assert.isTrue(utils.isBlank(output), 'Did not filter info message as expected.');
childLogger.info('this is my filtered the info message');
assert.isTrue(utils.isBlank(output), 'Did not filter info message as expected.');
logger.warning('this is the warning message');
let jsonObj = JSON.parse(output);
assert.strictEqual(jsonObj.msg, 'this is the warning message');
restartCustomWriter();
childLogger.warning('this is the child logger warning message');
jsonObj = JSON.parse(output);
assert.strictEqual(jsonObj.msg, 'this is the child logger warning message');
});
it('apply current log level to new created logger - INFO', () => {
assert.isTrue(utils.isBlank(output));
setGlobalLogLevel(LogLevel.WARNING);
const newLogger = createLogger('testing logger');
newLogger.info('this is the info message for newly created logger');
assert.isTrue(utils.isBlank(output), 'Did not filter info message as expected.');
newLogger.warning('this is the warning message');
const jsonObj = JSON.parse(output);
assert.strictEqual(jsonObj.msg, 'this is the warning message');
});
});
});
describe('Log to file', function () {
timeout(this, 30000);
let loggerConfig: LoggerConfigs;
let logger: ILogger;
let testingLogDir: string;
let testingLogFile: string;
before(async () => {
testingLogDir = `${constants.libRoot}/output/testingLogs`;
if (utils.isDir(testingLogDir)) {
await utils.clearDir(testingLogDir);
} else {
fs.mkdirsSync(testingLogDir);
}
testingLogFile = `${testingLogDir}/application.log`;
});
after(async () => {
await utils.deleteDir(testingLogDir);
});
beforeEach(async () => {
if (fs.existsSync(testingLogFile)) {
fs.unlinkSync(testingLogFile);
}
assert.isFalse(fs.existsSync(testingLogFile));
});
/**
* Logging with Pino is asynchronous.
* We need to make sure the log is written to file by waiting.
* @see https://github.com/pinojs/pino/blob/master/docs/asynchronous.md
*/
async function logAndWait(msg: string) {
logger.error(msg);
assert.isFunction(
(logger as any)[`pino`][`flush`],
`The "flush method should exist on a Pino logger"`,
);
(logger as any)[`pino`][`flush`]();
const max = 5000;
let elapsed = 0;
const sleep = 200;
while (!fs.existsSync(testingLogFile) && elapsed < max) {
await utils.sleep(sleep);
elapsed += sleep;
}
}
it('No log file - default', async () => {
loggerConfig = new LoggerConfigs(() => TESTING_CID);
loggerConfig.setLogHumanReadableinConsole(false);
loggerConfig.setLogSource(true);
loggerConfig.setLogLevel(LogLevel.DEBUG);
loggerConfig.setLogDirectory(testingLogDir);
initLogger(loggerConfig, 'default', true);
logger = new Logger('test');
await logAndWait('allo');
assert.isFalse(fs.existsSync(testingLogFile));
});
it('No log file - explicit', async () => {
loggerConfig = new LoggerConfigs(() => TESTING_CID);
loggerConfig.setLogHumanReadableinConsole(false);
loggerConfig.setLogSource(true);
loggerConfig.setLogLevel(LogLevel.DEBUG);
loggerConfig.setLogDirectory(testingLogDir);
loggerConfig.setSlowerLogToFileToo(false);
initLogger(loggerConfig, 'default', true);
logger = new Logger('test');
await logAndWait('allo');
assert.isFalse(fs.existsSync(testingLogFile));
});
it('Log file', async () => {
loggerConfig = new LoggerConfigs(() => TESTING_CID);
loggerConfig.setLogHumanReadableinConsole(false);
loggerConfig.setLogSource(true);
loggerConfig.setLogLevel(LogLevel.DEBUG);
loggerConfig.setLogDirectory(testingLogDir);
loggerConfig.setSlowerLogToFileToo(true);
initLogger(loggerConfig, 'default', true);
logger = new Logger('test');
await logAndWait('allo');
assert.isTrue(fs.existsSync(testingLogFile));
const content = fs.readFileSync(testingLogFile, 'utf-8');
assert.isOk(content);
assert.isTrue(content.indexOf(`"msg":"allo"`) > -1);
});
});
});