diff --git a/lib/categories.js b/lib/categories.js index 02daa99..f983bf6 100644 --- a/lib/categories.js +++ b/lib/categories.js @@ -5,6 +5,58 @@ const appenders = require('./appenders'); const categories = new Map(); +/** + * Add inherited config to this category. That includes extra appenders from parent, + * and level, if none is set on this category. + * This is recursive, so each parent also gets loaded with inherited appenders. + * Inheritance is blocked if a category has inherit=false + * @param {any} config + * @param {any} category the child category + * @param {string} categoryName dotted path to category + * @return {void} + */ +function inheritFromParent(config, category, categoryName) { + if (category.inherit === false) return; + const lastDotIndex = categoryName.lastIndexOf('.'); + if (lastDotIndex < 0) return; // category is not a child + const parentCategoryName = categoryName.substring(0, lastDotIndex); + let parentCategory = config.categories[parentCategoryName]; + if (!parentCategory) { + parentCategory = { inherit: true }; + config.categories[parentCategoryName] = parentCategory; + } + // make sure parent has had its inheritance taken care of before pulling its properties to this child + inheritFromParent(config, parentCategory, parentCategoryName); + + category.appenders = category.appenders || []; + category.level = category.level || parentCategory.level; + + // merge in appenders from parent (parent is already holding its inherited appenders) + parentCategory.appenders.forEach((ap) => { + if (!category.appenders.includes(ap)) { + category.appenders.push(ap); + } + }); + category.parent = parentCategory; +} + + +/** + * Walk all categories in the config, and pull down any configuration from parent to child. + * This includes inherited appenders, and level, where level is not set. + * Inheritance is skipped where a category has inherit=false. + * @param {any} config + */ +function addInheritedConfig(config) { + const categoryNames = Object.keys(config.categories); + categoryNames.forEach((name) => { + const category = config.categories[name]; + // add inherited appenders and level to this category + inheritFromParent(config, category, name); + }); +} + + configuration.addListener((config) => { configuration.throwExceptionIf( config, @@ -109,7 +161,7 @@ const setLevelForCategory = (category, level) => { if (!categoryConfig) { const sourceCategoryConfig = configForCategory(category); debug('setLevelForCategory: no config found for category, ' - + `found ${sourceCategoryConfig} for parents of ${category}`); + + `found ${sourceCategoryConfig} for parents of ${category}`); categoryConfig = { appenders: sourceCategoryConfig.appenders }; } categoryConfig.level = level; @@ -117,6 +169,7 @@ const setLevelForCategory = (category, level) => { }; module.exports = { + addInheritedConfig, appendersForCategory, getLevelForCategory, setLevelForCategory diff --git a/lib/log4js.js b/lib/log4js.js index 5883c47..7b357d9 100644 --- a/lib/log4js.js +++ b/lib/log4js.js @@ -59,6 +59,8 @@ function configure(configurationFileOrObject) { } debug(`Configuration is ${configObject}`); + categories.addInheritedConfig(configObject); // copy config from parent to child categories + configuration.configure(deepClone(configObject)); clustering.onMessage(sendLogEventToAppender); diff --git a/test/tap/configuration-inheritance-test.js b/test/tap/configuration-inheritance-test.js new file mode 100644 index 0000000..6503533 --- /dev/null +++ b/test/tap/configuration-inheritance-test.js @@ -0,0 +1,244 @@ +'use strict'; + +const test = require('tap').test; +// const util = require('util'); +// const debug = require('debug')('log4js:test.configuration-inheritance'); +const log4js = require('../../lib/log4js'); +// const configuration = require('../../lib/configuration'); + + +test('log4js category inherit all appenders from direct parent', (batch) => { + batch.test('should inherit appenders from direct parent', (t) => { + const config = { + appenders: { + stdout1: { type: 'stdout' }, + stdout2: { type: 'stdout' } + }, + categories: { + default: { appenders: ['stdout1'], level: 'ERROR' }, + catA: { appenders: ['stdout1', 'stdout2'], level: 'INFO' }, + 'catA.catB': { level: 'DEBUG' } + } + }; + + log4js.configure(config); + + const child = config.categories['catA.catB']; + t.ok(child); + t.ok(child.appenders); + t.isEqual(child.appenders.length, 2, 'inherited 2 appenders'); + t.ok(child.appenders.includes('stdout1'), 'inherited stdout1'); + t.ok(child.appenders.includes('stdout2'), 'inherited stdout2'); + t.isEqual(child.level, 'DEBUG', 'child level overrides parent'); + t.end(); + }); + + batch.test('multiple children should inherit config from shared parent', (t) => { + const config = { + appenders: { + stdout1: { type: 'stdout' }, + stdout2: { type: 'stdout' } + }, + categories: { + default: { appenders: ['stdout1'], level: 'ERROR' }, + catA: { appenders: ['stdout1'], level: 'INFO' }, + 'catA.catB.cat1': { level: 'DEBUG' }, // should get sdtout1, DEBUG + 'catA.catB.cat2': { appenders: ['stdout2'] } // should get sdtout1,sdtout2, INFO + } + }; + + log4js.configure(config); + + const child1 = config.categories['catA.catB.cat1']; + t.ok(child1); + t.ok(child1.appenders); + t.isEqual(child1.appenders.length, 1, 'inherited 1 appender'); + t.ok(child1.appenders.includes('stdout1'), 'inherited stdout1'); + t.isEqual(child1.level, 'DEBUG', 'child level overrides parent'); + + const child2 = config.categories['catA.catB.cat2']; + t.ok(child2); + t.ok(child2.appenders); + t.isEqual(child2.appenders.length, 2, 'inherited 1 appenders, plus its original'); + t.ok(child2.appenders.includes('stdout1'), 'inherited stdout1'); + t.ok(child2.appenders.includes('stdout2'), 'kept stdout2'); + t.isEqual(child2.level, 'INFO', 'inherited parent level'); + + t.end(); + }); + + batch.test('should inherit appenders from multiple parents', (t) => { + const config = { + appenders: { + stdout1: { type: 'stdout' }, + stdout2: { type: 'stdout' } + }, + categories: { + default: { appenders: ['stdout1'], level: 'ERROR' }, + catA: { appenders: ['stdout1'], level: 'INFO' }, + 'catA.catB': { appenders: ['stdout2'], level: 'INFO' }, // should get stdout1 and stdout2 + 'catA.catB.catC': { level: 'DEBUG' } // should get stdout1 and stdout2 + } + }; + + log4js.configure(config); + + const child = config.categories['catA.catB.catC']; + t.ok(child); + t.ok(child.appenders); + t.isEqual(child.appenders.length, 2, 'inherited 2 appenders'); + t.ok(child.appenders.includes('stdout1'), 'inherited stdout1'); + t.ok(child.appenders.includes('stdout1'), 'inherited stdout1'); + + const firstParent = config.categories['catA.catB']; + t.ok(firstParent); + t.ok(firstParent.appenders); + t.isEqual(firstParent.appenders.length, 2, 'ended up with 2 appenders'); + t.ok(firstParent.appenders.includes('stdout1'), 'inherited stdout1'); + t.ok(firstParent.appenders.includes('stdout2'), 'kept stdout2'); + + + t.end(); + }); + + batch.test('should inherit appenders from deep parent with missing direct parent', (t) => { + const config = { + appenders: { + stdout1: { type: 'stdout' }, + stdout2: { type: 'stdout' } + }, + categories: { + default: { appenders: ['stdout1'], level: 'ERROR' }, + catA: { appenders: ['stdout1'], level: 'INFO' }, + // no catA.catB, but should get created, with stdout1 + 'catA.catB.catC': { level: 'DEBUG' } // should get stdout1 + } + }; + + log4js.configure(config); + + const child = config.categories['catA.catB.catC']; + t.ok(child); + t.ok(child.appenders); + t.isEqual(child.appenders.length, 1, 'inherited 1 appenders'); + t.ok(child.appenders.includes('stdout1'), 'inherited stdout1'); + + const firstParent = config.categories['catA.catB']; + t.ok(firstParent); + t.ok(firstParent.appenders, 'catA.catB got created implicitily'); + t.isEqual(firstParent.appenders.length, 1, 'created with 1 inherited appender'); + t.ok(firstParent.appenders.includes('stdout1'), 'inherited stdout1'); + + t.end(); + }); + + + batch.test('should not get duplicate appenders if parent has the same one', (t) => { + const config = { + appenders: { + stdout1: { type: 'stdout' }, + stdout2: { type: 'stdout' } + }, + categories: { + default: { appenders: ['stdout1'], level: 'ERROR' }, + catA: { appenders: ['stdout1', 'stdout2'], level: 'INFO' }, + 'catA.catB': { appenders: ['stdout1'], level: 'DEBUG' } + } + }; + + log4js.configure(config); + + const child = config.categories['catA.catB']; + t.ok(child); + t.ok(child.appenders); + t.isEqual(child.appenders.length, 2, 'inherited 1 appender'); + t.ok(child.appenders.includes('stdout1'), 'still have stdout1'); + t.ok(child.appenders.includes('stdout2'), 'inherited stdout2'); + t.end(); + }); + + batch.test('inherit:falses should disable inheritance', (t) => { + const config = { + appenders: { + stdout1: { type: 'stdout' }, + stdout2: { type: 'stdout' } + }, + categories: { + default: { appenders: ['stdout1'], level: 'ERROR' }, + catA: { appenders: ['stdout1'], level: 'INFO' }, + 'catA.catB': { appenders: ['stdout2'], level: 'INFO', inherit: false }, // should not inherit from catA + } + }; + + log4js.configure(config); + + const child = config.categories['catA.catB']; + t.ok(child); + t.ok(child.appenders); + t.isEqual(child.appenders.length, 1, 'inherited no appender'); + t.ok(child.appenders.includes('stdout2'), 'kept stdout2'); + + t.end(); + }); + + + batch.test('inheritance should stop if direct parent has inherit off', (t) => { + const config = { + appenders: { + stdout1: { type: 'stdout' }, + stdout2: { type: 'stdout' } + }, + categories: { + default: { appenders: ['stdout1'], level: 'ERROR' }, + catA: { appenders: ['stdout1'], level: 'INFO' }, + 'catA.catB': { appenders: ['stdout2'], level: 'INFO', inherit: false }, // should not inherit from catA + 'catA.catB.catC': { level: 'DEBUG' } // should inherit from catB only + } + }; + + log4js.configure(config); + + const child = config.categories['catA.catB.catC']; + t.ok(child); + t.ok(child.appenders); + t.isEqual(child.appenders.length, 1, 'inherited 1 appender'); + t.ok(child.appenders.includes('stdout2'), 'inherited stdout2'); + + const firstParent = config.categories['catA.catB']; + t.ok(firstParent); + t.ok(firstParent.appenders); + t.isEqual(firstParent.appenders.length, 1, 'did not inherit new appenders'); + t.ok(firstParent.appenders.includes('stdout2'), 'kept stdout2'); + + t.end(); + }); + + batch.test('should inherit level when it is missing', (t) => { + const config = { + appenders: { + stdout1: { type: 'stdout' }, + stdout2: { type: 'stdout' } + }, + categories: { + default: { appenders: ['stdout1'], level: 'ERROR' }, + catA: { appenders: ['stdout1'], level: 'INFO' }, + // no catA.catB, but should get created, with stdout1, level INFO + 'catA.catB.catC': {} // should get stdout1, level INFO + } + }; + + log4js.configure(config); + + const child = config.categories['catA.catB.catC']; + t.ok(child); + t.isEqual(child.level, 'INFO', 'inherited level'); + + const firstParent = config.categories['catA.catB']; + t.ok(firstParent); + t.isEqual(firstParent.level, 'INFO', 'generate parent inherited level from base'); + + t.end(); + }); + + batch.end(); +});