UNPKG

@salesforce/core

Version:

Core libraries to interact with SFDX projects, orgs, and APIs.

447 lines 17.8 kB
"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