@zowe/imperative
Version:
framework for building configurable CLIs
481 lines • 22.8 kB
JavaScript
"use strict";
/*
* This program and the accompanying materials are made available under the terms of the
* Eclipse Public License v2.0 which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-v20.html
*
* SPDX-License-Identifier: EPL-2.0
*
* Copyright Contributors to the Zowe Project.
*
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.Logger = void 0;
const util_1 = require("util");
const ImperativeError_1 = require("../../error/src/ImperativeError");
const StackTrace = require("stack-trace");
const path = require("path");
const TextUtils_1 = require("../../utilities/src/TextUtils");
const io_1 = require("../../io");
const LoggerManager_1 = require("./LoggerManager");
const log4js = require("log4js");
const winston = require("winston");
const log4jsToWinston_1 = require("./log4jsToWinston");
const Console_1 = require("../../console/src/Console");
const censor_1 = require("../../censor");
/**
* Note(Kelosky): it seems from the log4js doc that you only get a single
* instance of log4js per category. To reconfigure, you should "shutdown" logger.
*/
class Logger {
/**
* Get accessibility to logging service to invoke log calls, e.g
* Logger.getLogger.info("important log info goes here");
* @param {string} category - category of logger to obtain
* @return {Logger} - instance of logger set to our app's category
*/
static getLoggerCategory(category) {
if (category === Logger.DEFAULT_CONSOLE_NAME) {
return new Logger(new Console_1.Console(), Logger.DEFAULT_CONSOLE_NAME);
}
else if (winston.loggers.has(category)) {
return new Logger(winston.loggers.get(category));
}
else {
return new Logger(log4js.getLogger(category), category);
}
}
/**
* Get accessibility to logging service to invoke log calls, e.g
* Logger.getLogger.info("important log info goes here");
* @return {Logger} - instance of logger set to our app's category
*/
static getImperativeLogger() {
return Logger.getLoggerCategory(Logger.DEFAULT_IMPERATIVE_NAME);
}
/**
* Get log4js instance directed at our app's category.
* @return {Logger} - instance of logger set to our app's category
*/
static getAppLogger() {
return Logger.getLoggerCategory(Logger.DEFAULT_APP_NAME);
}
static setLogInMemory(status, maxQueueSize) {
LoggerManager_1.LoggerManager.instance.logInMemory = status;
if (maxQueueSize != null) {
LoggerManager_1.LoggerManager.instance.maxQueueSize = maxQueueSize;
}
}
/**
* Write all messages that was stored in memory to the input file.
* @param {string} file - destination file name
*/
static writeInMemoryMessages(file) {
LoggerManager_1.LoggerManager.instance.dumpQueuedMessages(file);
}
/**
* Test if the input level is a valid value for Log4js.
* @param {string} testLevel - input level to be tested
* @returns {boolean} - status if the input level is valid
*/
static isValidLevel(testLevel) {
let status = false;
if (testLevel != null &&
Logger.DEFAULT_VALID_LOG_LEVELS.indexOf(testLevel.toUpperCase()) > -1) {
status = true;
}
return status;
}
/**
* Return an instance to the console logger which applies TextUtils invoked
* through this Logger class.
*
* Note(Kelosky): this is not the same as obtaining a new Console() directly,
* since we can make use of the internationalization and other routines
* within this Logger class via this implementation.
*
* @return {Logger} - instance of logger set to our app's category
*/
static getConsoleLogger() {
return Logger.getLoggerCategory(Logger.DEFAULT_CONSOLE_NAME);
}
/**
* Initializes a Logger powered by log4js, given a configuration.
* @param {IConfigLogging} loggingConfig The log4js configuration to use
* @return {Logger} A new logger instance
*/
static initLogger(loggingConfig) {
if (loggingConfig == null) {
throw new ImperativeError_1.ImperativeError({ msg: "Input logging config document is required" });
}
if (loggingConfig.log4jsConfig == null) {
throw new ImperativeError_1.ImperativeError({ msg: "Input logging config is incomplete, does not contain log4jsConfig" });
}
if (loggingConfig.log4jsConfig.appenders == null) {
throw new ImperativeError_1.ImperativeError({ msg: "Input logging config is incomplete, does not contain log4jsConfig.appenders" });
}
let logger;
try {
for (const appenderName of Object.keys(loggingConfig.log4jsConfig.appenders)) {
const appender = loggingConfig.log4jsConfig.appenders[appenderName];
if (appender.type === "file" || appender.type === "fileSync") {
io_1.IO.createDirsSyncFromFilePath(appender.filename);
}
}
log4js.configure(loggingConfig.log4jsConfig);
logger = log4js.getLogger();
logger.level = "debug";
LoggerManager_1.LoggerManager.instance.isLoggerInit = true;
return new Logger(logger);
}
catch (err) {
const cons = new Console_1.Console();
cons.error("Couldn't make desired logger: %s", (0, util_1.inspect)(err));
return new Logger(cons);
}
}
/**
* Creates an instance of a Logger powered by Winston, based on a log4js config.
* @param {IConfigLogging} loggingConfig The log4js configuration to use
* @return {Logger} A new logger instance
*/
static fromLog4jsToWinston(loggingConfig) {
var _a, _b, _c;
if (loggingConfig == null) {
throw new ImperativeError_1.ImperativeError({ msg: "Input logging config document is required" });
}
if (loggingConfig.log4jsConfig == null) {
throw new ImperativeError_1.ImperativeError({ msg: "Input logging config is incomplete, does not contain log4jsConfig" });
}
// log4js doc: When defining your appenders through a configuration, at least one category must be defined.
if (loggingConfig.log4jsConfig.appenders == null || loggingConfig.log4jsConfig.categories == null ||
Object.keys(loggingConfig.log4jsConfig.categories).length === 0) {
throw new ImperativeError_1.ImperativeError({ msg: "Input logging config is incomplete, does not contain log4jsConfig.appenders or log4jsConfig.categories" });
}
try {
for (const appenderName of Object.keys(loggingConfig.log4jsConfig.appenders)) {
const appender = loggingConfig.log4jsConfig.appenders[appenderName];
if (typeof appender === "object" &&
appender !== null &&
"type" in appender &&
(appender.type === "file" || appender.type === "fileSync") &&
"filename" in appender) {
io_1.IO.createDirsSyncFromFilePath(appender.filename);
}
}
let newLoggerInst;
// Process categories to create specific logger configurations
if (loggingConfig.log4jsConfig.categories) {
for (const categoryName of Object.keys(loggingConfig.log4jsConfig.categories)) {
const catConfig = loggingConfig.log4jsConfig.categories[categoryName];
const categoryLevel = ((catConfig === null || catConfig === void 0 ? void 0 : catConfig.level) || "info").toLowerCase(); // Default to info if level not specified
const categoryAppenders = (_a = catConfig === null || catConfig === void 0 ? void 0 : catConfig.appenders) !== null && _a !== void 0 ? _a : [];
// Generate a Winston config specifically for this category's appenders and level
const categoryWinstonConfig = (0, log4jsToWinston_1.log4jsConfigToWinstonConfig)(loggingConfig.log4jsConfig, // Pass the full original config for appender lookup
categoryLevel, categoryAppenders);
// Add custom levels to the generated config
categoryWinstonConfig.levels = log4jsToWinston_1.customLevels.levels;
// Add the logger with its specific configuration
winston.loggers.add(categoryName, categoryWinstonConfig);
newLoggerInst = winston.loggers.get(categoryName);
}
}
LoggerManager_1.LoggerManager.instance.isLoggerInit = true;
// Return the new logger instance if built, otherwise fallback to an available logger
if (newLoggerInst) {
return new Logger(newLoggerInst);
}
else {
// Fallback if new logger wasn't created or initialization failed
// First try the app winston logger, then the imperative winston logger - use a new console instance as last resort
const fallbackLogger = (_c = (_b = winston.loggers.get(Logger.DEFAULT_APP_NAME)) !== null && _b !== void 0 ? _b : winston.loggers.get(Logger.DEFAULT_IMPERATIVE_NAME)) !== null && _c !== void 0 ? _c : new Console_1.Console();
return new Logger(fallbackLogger);
}
}
catch (err) {
const cons = new Console_1.Console();
cons.error("Couldn't make desired logger: %s", (0, util_1.inspect)(err));
return new Logger(cons);
}
}
/**
* Creates a new Logger instance from a given Winston configuration.
* @param {winston.LoggerOptions} config - The Winston logger configuration options.
* @param {string} [category] - Optional category name for the logger.
* @returns {Logger} A new Logger instance.
*/
static fromWinstonConfig(config, category) {
try {
// Add custom levels to the provided config
const configWithCustomLevels = Object.assign(Object.assign({}, config), { levels: log4jsToWinston_1.customLevels.levels });
const winstonLogger = winston.createLogger(configWithCustomLevels);
// Optionally register the logger if a category is provided and categories are managed
if (category && configWithCustomLevels.levels && configWithCustomLevels.level) {
if (!winston.loggers.has(category)) {
winston.loggers.add(category, configWithCustomLevels);
}
// Ensure the created logger instance reflects the specified level for the category
winstonLogger.level = configWithCustomLevels.level;
}
return new Logger(winstonLogger, category);
}
catch (err) {
// Fallback or error handling, potentially log using a default console logger
const cons = new Console_1.Console();
cons.error("Failed to create logger from Winston config: %s", (0, util_1.inspect)(err));
// Return a console logger as a fallback
return new Logger(cons, category !== null && category !== void 0 ? category : Logger.DEFAULT_CONSOLE_NAME);
}
}
constructor(mJsLogger, category) {
this.mJsLogger = mJsLogger;
this.category = category;
if (LoggerManager_1.LoggerManager.instance.isLoggerInit && LoggerManager_1.LoggerManager.instance.QueuedMessages.length > 0) {
LoggerManager_1.LoggerManager.instance.QueuedMessages.slice().reverse().forEach((value) => {
if (this.category === value.category) {
mJsLogger.log(value.method, value.message);
LoggerManager_1.LoggerManager.instance.QueuedMessages.splice(LoggerManager_1.LoggerManager.instance.QueuedMessages.indexOf(value), 1);
}
});
}
this.initStatus = LoggerManager_1.LoggerManager.instance.isLoggerInit;
}
// TODO: Can we find trace info for TypeScript to have e.g. [ERROR] Jobs.ts : 43 - Error encountered
/**
* Log a message at the "trace" level
* Example: 'Entering cheese testing'
* @param message - printf style template string, or a plain string message
* @param args - printf style args
* @returns {any}
*/
trace(message, ...args) {
const finalMessage = TextUtils_1.TextUtils.formatMessage.apply(this, [message].concat(args));
if (LoggerManager_1.LoggerManager.instance.isLoggerInit || this.category === Logger.DEFAULT_CONSOLE_NAME) {
this.logService.log("trace", this.getCallerFileAndLineTag() + finalMessage);
}
else {
LoggerManager_1.LoggerManager.instance.queueMessage(this.category, "trace", this.getCallerFileAndLineTag() + finalMessage);
}
return finalMessage;
}
/**
* Log a message at the "debug" level
* Example: 'Got cheese'
* @param message - printf or mustache style template string, or a plain string message
* @param args - printf or mustache style args
* @returns {any}
*/
debug(message, ...args) {
const finalMessage = censor_1.Censor.censorRawData(TextUtils_1.TextUtils.formatMessage.apply(this, [message].concat(args)), this.category);
if (LoggerManager_1.LoggerManager.instance.isLoggerInit || this.category === Logger.DEFAULT_CONSOLE_NAME) {
this.logService.debug(this.getCallerFileAndLineTag() + finalMessage);
}
else {
LoggerManager_1.LoggerManager.instance.queueMessage(this.category, "debug", this.getCallerFileAndLineTag() + finalMessage);
}
return finalMessage;
}
/**
* Log a message at the "info" level
* Example: 'Cheese is Gouda'
* @param message - printf or mustache style template string, or a plain string message
* @param args - printf or mustache style args
* @returns {any}
*/
info(message, ...args) {
const finalMessage = censor_1.Censor.censorRawData(TextUtils_1.TextUtils.formatMessage.apply(this, [message].concat(args)), this.category);
if (LoggerManager_1.LoggerManager.instance.isLoggerInit || this.category === Logger.DEFAULT_CONSOLE_NAME) {
this.logService.info(this.getCallerFileAndLineTag() + finalMessage);
}
else {
LoggerManager_1.LoggerManager.instance.queueMessage(this.category, "info", this.getCallerFileAndLineTag() + finalMessage);
}
return finalMessage;
}
/**
* Log a message at the "warn" level
* Example: 'Cheese is quite smelly.'
* @param message - printf or mustache style template string, or a plain string message
* @param args - printf or mustache style args
* @returns {any}
*/
warn(message, ...args) {
const finalMessage = censor_1.Censor.censorRawData(TextUtils_1.TextUtils.formatMessage.apply(this, [message].concat(args)), this.category);
if (LoggerManager_1.LoggerManager.instance.isLoggerInit || this.category === Logger.DEFAULT_CONSOLE_NAME) {
this.logService.warn(this.getCallerFileAndLineTag() + finalMessage);
}
else {
LoggerManager_1.LoggerManager.instance.queueMessage(this.category, "warn", this.getCallerFileAndLineTag() + finalMessage);
}
return finalMessage;
}
/**
* Log a message at the "error" level
* Example: 'Cheese is too ripe!'
* @param message - printf or mustache style template string, or a plain string message
* @param args - printf or mustache style args
* @returns {any}
*/
error(message, ...args) {
const finalMessage = censor_1.Censor.censorRawData(TextUtils_1.TextUtils.formatMessage.apply(this, [message].concat(args)), this.category);
if (LoggerManager_1.LoggerManager.instance.isLoggerInit || this.category === Logger.DEFAULT_CONSOLE_NAME) {
this.logService.error(this.getCallerFileAndLineTag() + finalMessage);
}
else {
LoggerManager_1.LoggerManager.instance.queueMessage(this.category, "error", this.getCallerFileAndLineTag() + finalMessage);
}
return finalMessage;
}
/**
* Log a message at the "fatal" level
* Example: 'Cheese was breeding ground for listeria.'
* @param message - printf or mustache style template string, or a plain string message
* @param args - printf or mustache style args
* @returns {any}
*/
fatal(message, ...args) {
const finalMessage = censor_1.Censor.censorRawData(TextUtils_1.TextUtils.formatMessage.apply(this, [message].concat(args)), this.category);
if (LoggerManager_1.LoggerManager.instance.isLoggerInit || this.category === Logger.DEFAULT_CONSOLE_NAME) {
this.logService.log("fatal", this.getCallerFileAndLineTag() + finalMessage);
}
else {
LoggerManager_1.LoggerManager.instance.queueMessage(this.category, "fatal", this.getCallerFileAndLineTag() + finalMessage);
}
return finalMessage;
}
/**
* Log a message without CallerFileAndLineTag
* Example: 'Cheese that is plain'
* @param message - printf or mustache style template string, or a plain string message
* @param args - printf or mustache style args
* @returns {any}
*/
simple(message, ...args) {
const finalMessage = censor_1.Censor.censorRawData(TextUtils_1.TextUtils.formatMessage.apply(this, [message].concat(args)), this.category);
if (LoggerManager_1.LoggerManager.instance.isLoggerInit || this.category === Logger.DEFAULT_CONSOLE_NAME) {
this.logService.info(finalMessage);
}
else {
LoggerManager_1.LoggerManager.instance.queueMessage(this.category, "info", finalMessage);
}
return finalMessage;
}
/**
* Log an Imperative error, including any optional fields if present
* @param {ImperativeError} err - the error to log
*/
logError(err) {
this.debug("Stack at time of error logging: %s", new Error().stack);
if (!(err.details.additionalDetails == null)) {
this.error(err.details.additionalDetails);
}
if (!(err.stack == null)) {
this.error(err.stack);
}
if (!(err.details.causeErrors == null) && !(err.details.causeErrors.length == null)
&& err.details.causeErrors.length > 0) {
for (const cause of err.details.causeErrors) {
this.error("Cause error:\n%s", (0, util_1.inspect)(cause));
}
}
this.error(err.message);
}
/**
* translate a message if possible
* @param message - original message to translate, possibly with printf or {{obj}} style template
* @param args - varargs to use to translate / format
* @returns {string} translated or replaced result
*/
// public translate(message: string, ...args: any[]): string {
// let result: string;
// let translationError: Error;
// try {
// result = i18n.__.apply(global, [message].concat(args));
// } catch (e) {
// result = undefined;
// translationError = e;
// }
// if (isNullOrUndefined(result)) {
// if (translationError) {
// this.logService.warn("Error while translating!\n%s", inspect(translationError));
// }
// result = TextUtils.formatMessage(message, ...args);
// }
// return result;
// }
/**
* Obtain .js file name and line number which issued the log message.
* NOTE(Kelosky): Consensus seems to be that this may produce a lot of overhead
* by creating an Error and obtaining stack information for EVERY log message
* that is issued.
*
* There are also packages available to obtain the appropriate line number.
*
* Perhaps when a package pops up that gives the appropriate .ts line number
* and file name, we'll remove usage of this method.
* @returns {string} - file and line number
*/
getCallerFileAndLineTag() {
try {
const frame = StackTrace.parse(new Error());
let callerStackIndex = 1;
while (!frame[callerStackIndex].getFileName() || frame[callerStackIndex].getFileName().indexOf(path.basename(__filename)) >= 0) {
// go up the stack until we're outside of the Zowe Logger file
callerStackIndex += 1;
}
const filename = path.basename(frame[callerStackIndex].getFileName());
const lineNumber = frame[callerStackIndex].getLineNumber();
return (0, util_1.format)("[%s:%s] ", filename, lineNumber);
}
catch (e) {
return "[<unknown>] ";
}
}
/**
* Allow for programmatic adjustments to the logger
* @param {string} level - new level to set
*/
set level(level) {
// Update the level of the current logger instance
this.logService.level = level;
// If this is a Winston logger, update the level on the registered transports
if (this.logService instanceof winston.Logger) {
for (const transport of this.logService.transports) {
transport.level = level;
}
}
}
/**
* Get current level setting
* @return {string} - level of current log setting
*/
get level() {
return this.logService.level.toString().toUpperCase();
}
/**
* Get underlying logger service
*
* This function also check to see if log4js is configured since the last time it
* was called. If yes, then update the logger with to leverage the new configuration.
*/
get logService() {
if (this.initStatus !== LoggerManager_1.LoggerManager.instance.isLoggerInit) {
const newLogger = Logger.getLoggerCategory(this.category);
this.mJsLogger = newLogger.mJsLogger;
this.initStatus = newLogger.initStatus;
}
return this.mJsLogger;
}
/**
* Set underlying logger service
*/
set logService(service) {
this.mJsLogger = service;
}
}
exports.Logger = Logger;
Logger.DEFAULT_IMPERATIVE_NAME = "imperative";
Logger.DEFAULT_APP_NAME = "app";
Logger.DEFAULT_CONSOLE_NAME = "console";
Logger.DEFAULT_VALID_LOG_LEVELS = ["ALL", "TRACE", "DEBUG", "INFO", "WARN", "ERROR", "FATAL", "MARK", "OFF"];
//# sourceMappingURL=Logger.js.map