const debug = require('debug')('log4js:categories'); const configuration = require('./configuration'); const levels = require('./levels'); 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 {*} config * @param {*} 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.slice(0, lastDotIndex); let parentCategory = config.categories[parentCategoryName]; if (!parentCategory) { // parent is missing, so implicitly create it, so that it can inherit from its parents parentCategory = { inherit: true, appenders: [] }; } // make sure parent has had its inheritance taken care of before pulling its properties to this child inheritFromParent(config, parentCategory, parentCategoryName); // if the parent is not in the config (because we just created it above), // and it inherited a valid configuration, add it to config.categories if ( !config.categories[parentCategoryName] && parentCategory.appenders && parentCategory.appenders.length && parentCategory.level ) { config.categories[parentCategoryName] = parentCategory; } 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 {*} config */ function addCategoryInheritance(config) { if (!config.categories) return; 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.addPreProcessingListener((config) => addCategoryInheritance(config) ); configuration.addListener((config) => { configuration.throwExceptionIf( config, configuration.not(configuration.anObject(config.categories)), 'must have a property "categories" of type object.' ); const categoryNames = Object.keys(config.categories); configuration.throwExceptionIf( config, configuration.not(categoryNames.length), 'must define at least one category.' ); categoryNames.forEach((name) => { const category = config.categories[name]; configuration.throwExceptionIf( config, [ configuration.not(category.appenders), configuration.not(category.level), ], `category "${name}" is not valid (must be an object with properties "appenders" and "level")` ); configuration.throwExceptionIf( config, configuration.not(Array.isArray(category.appenders)), `category "${name}" is not valid (appenders must be an array of appender names)` ); configuration.throwExceptionIf( config, configuration.not(category.appenders.length), `category "${name}" is not valid (appenders must contain at least one appender name)` ); if (Object.prototype.hasOwnProperty.call(category, 'enableCallStack')) { configuration.throwExceptionIf( config, typeof category.enableCallStack !== 'boolean', `category "${name}" is not valid (enableCallStack must be boolean type)` ); } category.appenders.forEach((appender) => { configuration.throwExceptionIf( config, configuration.not(appenders.get(appender)), `category "${name}" is not valid (appender "${appender}" is not defined)` ); }); configuration.throwExceptionIf( config, configuration.not(levels.getLevel(category.level)), `category "${name}" is not valid (level "${category.level}" not recognised;` + ` valid levels are ${levels.levels.join(', ')})` ); }); configuration.throwExceptionIf( config, configuration.not(config.categories.default), 'must define a "default" category.' ); }); const setup = (config) => { categories.clear(); if (!config) { return; } const categoryNames = Object.keys(config.categories); categoryNames.forEach((name) => { const category = config.categories[name]; const categoryAppenders = []; category.appenders.forEach((appender) => { categoryAppenders.push(appenders.get(appender)); debug(`Creating category ${name}`); categories.set(name, { appenders: categoryAppenders, level: levels.getLevel(category.level), enableCallStack: category.enableCallStack || false, }); }); }); }; const init = () => { setup(); }; init(); configuration.addListener(setup); const configForCategory = (category) => { debug(`configForCategory: searching for config for ${category}`); if (categories.has(category)) { debug(`configForCategory: ${category} exists in config, returning it`); return categories.get(category); } let sourceCategoryConfig; if (category.indexOf('.') > 0) { debug(`configForCategory: ${category} has hierarchy, cloning from parents`); sourceCategoryConfig = { ...configForCategory(category.slice(0, category.lastIndexOf('.'))), }; } else { if (!categories.has('default')) { setup({ categories: { default: { appenders: ['out'], level: 'OFF' } } }); } debug('configForCategory: cloning default category'); sourceCategoryConfig = { ...categories.get('default') }; } categories.set(category, sourceCategoryConfig); return sourceCategoryConfig; }; const appendersForCategory = (category) => configForCategory(category).appenders; const getLevelForCategory = (category) => configForCategory(category).level; const setLevelForCategory = (category, level) => { configForCategory(category).level = level; }; const getEnableCallStackForCategory = (category) => configForCategory(category).enableCallStack === true; const setEnableCallStackForCategory = (category, useCallStack) => { configForCategory(category).enableCallStack = useCallStack; }; module.exports = categories; module.exports = Object.assign(module.exports, { appendersForCategory, getLevelForCategory, setLevelForCategory, getEnableCallStackForCategory, setEnableCallStackForCategory, init, });