@salesforce/core
Version:
Core libraries to interact with SFDX projects, orgs, and APIs.
447 lines • 17.8 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.computeLevel = exports.Logger = exports.LoggerLevel = void 0;
/*
* Copyright (c) 2020, salesforce.com, inc.
* All rights reserved.
* Licensed under the BSD 3-Clause license.
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/
const os = __importStar(require("node:os"));
const path = __importStar(require("node:path"));
const pino_1 = require("pino");
const kit_1 = require("@salesforce/kit");
const ts_types_1 = require("@salesforce/ts-types");
const global_1 = require("../global");
const sfError_1 = require("../sfError");
const unwrapArray_1 = require("../util/unwrapArray");
const memoryLogger_1 = require("./memoryLogger");
const cleanup_1 = require("./cleanup");
/**
* Standard `Logger` levels.
*
* **See** {@link https://getpino.io/#/docs/api?id=logger-level |Logger Levels}
*/
var LoggerLevel;
(function (LoggerLevel) {
LoggerLevel[LoggerLevel["TRACE"] = 10] = "TRACE";
LoggerLevel[LoggerLevel["DEBUG"] = 20] = "DEBUG";
LoggerLevel[LoggerLevel["INFO"] = 30] = "INFO";
LoggerLevel[LoggerLevel["WARN"] = 40] = "WARN";
LoggerLevel[LoggerLevel["ERROR"] = 50] = "ERROR";
LoggerLevel[LoggerLevel["FATAL"] = 60] = "FATAL";
})(LoggerLevel || (exports.LoggerLevel = LoggerLevel = {}));
/**
* A logging abstraction powered by {@link https://github.com/pinojs/pino | Pino} that provides both a default
* logger configuration that will log to the default path, and a way to create custom loggers based on the same foundation.
*
* ```
* // Gets the root sfdx logger
* const logger = await Logger.root();
*
* // Creates a child logger of the root sfdx logger with custom fields applied
* const childLogger = await Logger.child('myRootChild', {tag: 'value'});
*
* // Creates a custom logger unaffiliated with the root logger
* const myCustomLogger = new Logger('myCustomLogger');
*
* // Creates a child of a custom logger unaffiliated with the root logger with custom fields applied
* const myCustomChildLogger = myCustomLogger.child('myCustomChild', {tag: 'value'});
*
* // get a raw pino logger from the root instance of Logger
* // you can use these to avoid constructing another Logger wrapper class and to get better type support
* const logger = Logger.getRawRootLogger().child({name: 'foo', otherProp: 'bar'});
* logger.info({some: 'stuff'}, 'a message');
*
*
* // get a raw pino logger from the current instance
* const childLogger = await Logger.child('myRootChild', {tag: 'value'});
* const logger = childLogger.getRawLogger();
* ```
*
* **See** https://developer.salesforce.com/docs/atlas.en-us.sfdx_setup.meta/sfdx_setup/sfdx_dev_cli_log_messages.htm
*/
class Logger {
/**
* The name of the root sfdx `Logger`.
*/
static ROOT_NAME = 'sf';
/**
* The default `LoggerLevel` when constructing new `Logger` instances.
*/
static DEFAULT_LEVEL = LoggerLevel.WARN;
/**
* A list of all lower case `LoggerLevel` names.
*
* **See** {@link LoggerLevel}
*/
static LEVEL_NAMES = Object.values(LoggerLevel)
.filter(ts_types_1.isString)
.map((v) => v.toLowerCase());
// The sfdx root logger singleton
static rootLogger;
pinoLogger;
memoryLogger;
/**
* Constructs a new `Logger`.
*
* @param optionsOrName A set of `LoggerOptions` or name to use with the default options.
*
* **Throws** *{@link SfError}{ name: 'RedundantRootLoggerError' }* More than one attempt is made to construct the root
* `Logger`.
*/
constructor(optionsOrName) {
const enabled = process.env.SFDX_DISABLE_LOG_FILE !== 'true' && process.env.SF_DISABLE_LOG_FILE !== 'true';
const options = typeof optionsOrName === 'string'
? { name: optionsOrName, level: Logger.DEFAULT_LEVEL, fields: {} }
: optionsOrName;
if (Logger.rootLogger && options.name === Logger.ROOT_NAME) {
throw new sfError_1.SfError('Can not create another root logger.', 'RedundantRootLoggerError');
}
// if there is a rootLogger, use its Pino instance
if (Logger.rootLogger) {
this.pinoLogger = Logger.rootLogger.pinoLogger.child({ ...options.fields, name: options.name });
this.memoryLogger = Logger.rootLogger.memoryLogger; // if the root was constructed with memory logging, keep that
this.pinoLogger.trace(`Created '${options.name}' child logger instance`);
}
else {
const level = (0, exports.computeLevel)(options.level);
const commonOptions = {
name: options.name ?? Logger.ROOT_NAME,
base: options.fields ?? {},
level,
enabled,
};
if (Boolean(options.useMemoryLogger) || global_1.Global.getEnvironmentMode() === global_1.Mode.TEST || !enabled) {
this.memoryLogger = new memoryLogger_1.MemoryLogger();
this.pinoLogger = (0, pino_1.pino)(commonOptions, this.memoryLogger);
}
else {
this.pinoLogger = (0, pino_1.pino)({
...commonOptions,
transport: {
pipeline: [
{
// WARNING: Please make sure to bundle transformStream by referencing the correct path. Reach out to IDEx Foundations Team.
target: path.join('..', '..', 'lib', 'logger', 'transformStream'),
},
getWriteStream(level),
],
},
});
// when a new file logger root is instantiated, we check for old log files.
// but we don't want to wait for it
// and it's async and we can't wait from a ctor anyway
void (0, cleanup_1.cleanup)();
}
Logger.rootLogger = this;
}
}
/**
*
* Gets the root logger. It's a singleton
* See also getRawLogger if you don't need the root logger
*/
static async root() {
return Promise.resolve(this.getRoot());
}
/**
* Gets the root logger. It's a singleton
*/
static getRoot() {
if (this.rootLogger) {
return this.rootLogger;
}
const rootLogger = (this.rootLogger = new Logger(Logger.ROOT_NAME));
return rootLogger;
}
/**
* Destroys the root `Logger`.
*
* @ignore
*/
static destroyRoot() {
if (this.rootLogger) {
this.rootLogger = undefined;
}
}
/**
* Create a child of the root logger, inheriting this instance's configuration such as `level`, transports, etc.
*
* @param name The name of the child logger.
* @param fields Additional fields included in all log lines.
*/
static async child(name, fields) {
return (await Logger.root()).child(name, fields);
}
/**
* Create a child of the root logger, inheriting this instance's configuration such as `level`, transports, etc.
*
* @param name The name of the child logger.
* @param fields Additional fields included in all log lines.
*/
static childFromRoot(name, fields) {
return Logger.getRoot().child(name, fields);
}
/**
* Gets a numeric `LoggerLevel` value by string name.
*
* @param {string} levelName The level name to convert to a `LoggerLevel` enum value.
*
* **Throws** *{@link SfError}{ name: 'UnrecognizedLoggerLevelNameError' }* The level name was not case-insensitively recognized as a valid `LoggerLevel` value.
* @see {@Link LoggerLevel}
*/
static getLevelByName(levelName) {
const upperLevel = levelName.toUpperCase();
if (!(0, ts_types_1.isKeyOf)(LoggerLevel, upperLevel)) {
throw new sfError_1.SfError(`Invalid log level "${upperLevel}".`, 'UnrecognizedLoggerLevelNameError');
}
return LoggerLevel[upperLevel];
}
/** get the bare (pino) logger instead of using the class hierarchy */
static getRawRootLogger() {
return Logger.getRoot().pinoLogger;
}
/** get the bare (pino) logger instead of using the class hierarchy */
getRawLogger() {
return this.pinoLogger;
}
/**
* Gets the name of this logger.
*/
getName() {
return this.pinoLogger.bindings().name ?? '';
}
/**
* Gets the current level of this logger.
*/
getLevel() {
return this.pinoLogger.levelVal;
}
/**
* Set the logging level of all streams for this logger. If a specific `level` is not provided, this method will
* attempt to read it from the environment variable `SFDX_LOG_LEVEL`, and if not found,
* {@link Logger.DEFAULT_LOG_LEVEL} will be used instead. For convenience `this` object is returned.
*
* @param {LoggerLevelValue} [level] The logger level.
*
* **Throws** *{@link SfError}{ name: 'UnrecognizedLoggerLevelNameError' }* A value of `level` read from `SFDX_LOG_LEVEL`
* was invalid.
*
* ```
* // Sets the level from the environment or default value
* logger.setLevel()
*
* // Set the level from the INFO enum
* logger.setLevel(LoggerLevel.INFO)
*
* // Sets the level case-insensitively from a string value
* logger.setLevel(Logger.getLevelByName('info'))
* ```
*/
setLevel(level) {
this.pinoLogger.level =
this.pinoLogger.levels.labels[level ?? getDefaultLevel()] ?? this.pinoLogger.levels.labels[Logger.DEFAULT_LEVEL];
return this;
}
/**
* Compares the requested log level with the current log level. Returns true if
* the requested log level is greater than or equal to the current log level.
*
* @param level The requested log level to compare against the currently set log level.
*/
shouldLog(level) {
return (typeof level === 'string' ? this.pinoLogger.levelVal : level) >= this.getLevel();
}
/**
* Gets an array of log line objects. Each element is an object that corresponds to a log line.
*/
getBufferedRecords() {
if (!this.memoryLogger) {
throw new Error('getBufferedRecords is only supported when useMemoryLogging is true');
}
return this.memoryLogger?.loggedData ?? [];
}
/**
* Reads a text blob of all the log lines contained in memory or the log file.
*/
readLogContentsAsText() {
if (this.memoryLogger) {
return this.memoryLogger?.loggedData.map((line) => JSON.stringify(line)).join(os.EOL);
}
else {
this.pinoLogger.warn('readLogContentsAsText is not supported for file streams, only used when useMemoryLogging is true');
const content = '';
return content;
}
}
/**
* Create a child logger, typically to add a few log record fields. For convenience this object is returned.
*
* @param name The name of the child logger that is emitted w/ log line. Will be prefixed with the parent logger name and `:`
* @param fields Additional fields included in all log lines for the child logger.
*/
child(name, fields = {}) {
const fullName = `${this.getName()}:${name}`;
const child = new Logger({ name: fullName, fields });
this.pinoLogger.trace(`Setup child '${fullName}' logger instance`);
return child;
}
/**
* Add a field to all log lines for this logger. For convenience `this` object is returned.
*
* @param name The name of the field to add.
* @param value The value of the field to be logged.
*/
addField(name, value) {
this.pinoLogger.setBindings({ ...this.pinoLogger.bindings(), [name]: value });
return this;
}
/**
* Logs at `trace` level with filtering applied. For convenience `this` object is returned.
*
* @param args Any number of arguments to be logged.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
trace(...args) {
this.pinoLogger.trace((0, unwrapArray_1.unwrapArray)(args));
return this;
}
/**
* Logs at `debug` level with filtering applied. For convenience `this` object is returned.
*
* @param args Any number of arguments to be logged.
*/
debug(...args) {
this.pinoLogger.debug((0, unwrapArray_1.unwrapArray)(args));
return this;
}
/**
* Logs at `debug` level with filtering applied.
*
* @param cb A callback that returns on array objects to be logged.
*/
// eslint-disable-next-line class-methods-use-this, @typescript-eslint/no-unused-vars, @typescript-eslint/no-empty-function
debugCallback(cb) { }
/**
* Logs at `info` level with filtering applied. For convenience `this` object is returned.
*
* @param args Any number of arguments to be logged.
*/
info(...args) {
this.pinoLogger.info((0, unwrapArray_1.unwrapArray)(args));
return this;
}
/**
* Logs at `warn` level with filtering applied. For convenience `this` object is returned.
*
* @param args Any number of arguments to be logged.
*/
warn(...args) {
this.pinoLogger.warn((0, unwrapArray_1.unwrapArray)(args));
return this;
}
/**
* Logs at `error` level with filtering applied. For convenience `this` object is returned.
*
* @param args Any number of arguments to be logged.
*/
error(...args) {
this.pinoLogger.error((0, unwrapArray_1.unwrapArray)(args));
return this;
}
/**
* Logs at `fatal` level with filtering applied. For convenience `this` object is returned.
*
* @param args Any number of arguments to be logged.
*/
fatal(...args) {
// always show fatal to stderr
// IMPORTANT:
// Do not use console.error() here, if fatal() is called from the uncaughtException handler, it
// will be re-thrown and caught again by the uncaughtException handler, causing an infinite loop.
console.log(...args); // eslint-disable-line no-console
this.pinoLogger.fatal((0, unwrapArray_1.unwrapArray)(args));
return this;
}
}
exports.Logger = Logger;
/** return various streams that the logger could send data to, depending on the options and env */
const getWriteStream = (level = 'warn') => {
// used when debug mode, writes to stdout (colorized)
if (process.env.DEBUG) {
return {
target: 'pino-pretty',
options: { colorize: true },
};
}
// default: we're writing to a rotating file
const rotator = new Map([
['1m', new Date().toISOString().split(':').slice(0, 2).join('-')],
['1h', new Date().toISOString().split(':').slice(0, 1).join('-')],
['1d', new Date().toISOString().split('T')[0]],
]);
const logRotationPeriod = new kit_1.Env().getString('SF_LOG_ROTATION_PERIOD') ?? '1d';
return {
// write to a rotating file
target: 'pino/file',
options: {
destination: path.join(global_1.Global.SF_DIR, `sf-${rotator.get(logRotationPeriod) ?? rotator.get('1d')}.log`),
mkdir: true,
level,
},
};
};
const computeLevel = (optionsLevel) => {
const env = new kit_1.Env();
const envValue = isNaN(env.getNumber('SF_LOG_LEVEL') ?? NaN)
? env.getString('SF_LOG_LEVEL')
: env.getNumber('SF_LOG_LEVEL');
if (typeof envValue !== 'undefined') {
return typeof envValue === 'string' ? envValue : numberToLevel(envValue);
}
return levelFromOption(optionsLevel);
};
exports.computeLevel = computeLevel;
const levelFromOption = (value) => {
switch (typeof value) {
case 'number':
return numberToLevel(value);
case 'string':
return value;
default:
return pino_1.pino.levels.labels[Logger.DEFAULT_LEVEL];
}
};
// /** match a number to a pino level, or if a match isn't found, the next highest level */
const numberToLevel = (level) => pino_1.pino.levels.labels[level] ??
Object.entries(pino_1.pino.levels.labels).find(([value]) => Number(value) > level)?.[1] ??
'warn';
const getDefaultLevel = () => {
const logLevelFromEnvVar = new kit_1.Env().getString('SF_LOG_LEVEL');
return logLevelFromEnvVar ? Logger.getLevelByName(logLevelFromEnvVar) : Logger.DEFAULT_LEVEL;
};
//# sourceMappingURL=logger.js.map