From 1f527330f060ea47044c8245b62f5bdd08c65dae Mon Sep 17 00:00:00 2001 From: Cristian Dominguez Date: Thu, 22 Jun 2023 15:37:38 -0300 Subject: [PATCH 01/37] fix: bump jsforce --- package.json | 4 ++-- src/org/authInfo.ts | 22 +++++++++++++++++----- src/org/scratchOrgInfoApi.ts | 3 ++- src/org/scratchOrgLifecycleEvents.ts | 8 ++++---- src/util/jsonXmlTools.ts | 3 ++- src/webOAuthServer.ts | 3 ++- test/unit/org/authInfoTest.ts | 3 ++- yarn.lock | 8 ++++---- 8 files changed, 35 insertions(+), 19 deletions(-) diff --git a/package.json b/package.json index c5f92b1c0f..19efce563a 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,7 @@ "faye": "^1.4.0", "form-data": "^4.0.0", "js2xmlparser": "^4.0.1", - "jsforce": "^2.0.0-beta.24", + "jsforce": "^2.0.0-beta.25", "jsonwebtoken": "9.0.0", "jszip": "3.10.1", "proper-lockfile": "^4.1.2", @@ -171,4 +171,4 @@ ] } } -} \ No newline at end of file +} diff --git a/src/org/authInfo.ts b/src/org/authInfo.ts index 0dd278cc36..1cc6f0c590 100644 --- a/src/org/authInfo.ts +++ b/src/org/authInfo.ts @@ -25,7 +25,7 @@ import { Nullable, Optional, } from '@salesforce/ts-types'; -import { JwtOAuth2, JwtOAuth2Config, OAuth2, TokenResponse } from 'jsforce'; +import { OAuth2Config, OAuth2, TokenResponse } from 'jsforce'; import Transport from 'jsforce/lib/transport'; import * as jwt from 'jsonwebtoken'; import { Config } from '../config/config'; @@ -111,6 +111,14 @@ export type AuthSideEffects = { setTracksSource?: boolean; }; +export type JwtOAuth2Config = OAuth2Config & { + privateKey?: string; + privateKeyFile?: string; + authCode?: string; + refreshToken?: string; + username?: string; +}; + type UserInfo = AnyJson & { username: string; organizationId: string; @@ -938,10 +946,14 @@ export class AuthInfo extends AsyncOptionalCreatable { } ); - const oauth2 = new JwtOAuth2({ loginUrl }); - // jsforce has it types as any - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - return ensureJsonMap(await oauth2.jwtAuthorize(jwtToken)); + const oauth2 = new OAuth2({ loginUrl }); + return ensureJsonMap( + await oauth2.requestToken({ + // eslint-disable-next-line camelcase + grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer', + assertion: jwtToken, + }) + ); } // Build OAuth config for a refresh token auth flow diff --git a/src/org/scratchOrgInfoApi.ts b/src/org/scratchOrgInfoApi.ts index 47ebb5fec5..9e42278659 100644 --- a/src/org/scratchOrgInfoApi.ts +++ b/src/org/scratchOrgInfoApi.ts @@ -7,8 +7,9 @@ import { env, Duration, upperFirst } from '@salesforce/kit'; import { AnyJson } from '@salesforce/ts-types'; -import { OAuth2Config, JwtOAuth2Config, SaveResult } from 'jsforce'; +import { OAuth2Config, SaveResult } from 'jsforce'; import { retryDecorator, RetryError } from 'ts-retry-promise'; +import { JwtOAuth2Config } from '../org/authInfo'; import { Logger } from '../logger'; import { Messages } from '../messages'; import { SfError } from '../sfError'; diff --git a/src/org/scratchOrgLifecycleEvents.ts b/src/org/scratchOrgLifecycleEvents.ts index 32cbc6a29a..4d8e4470a2 100644 --- a/src/org/scratchOrgLifecycleEvents.ts +++ b/src/org/scratchOrgLifecycleEvents.ts @@ -21,7 +21,7 @@ export const scratchOrgLifecycleStages = [ 'done', ] as const; export interface ScratchOrgLifecycleEvent { - stage: typeof scratchOrgLifecycleStages[number]; + stage: (typeof scratchOrgLifecycleStages)[number]; scratchOrgInfo?: ScratchOrgInfo; } @@ -41,10 +41,10 @@ const postOrgCreateHookFields = [ 'username', ] as const; -type PostOrgCreateHook = Pick; +type PostOrgCreateHook = Pick; -const isHookField = (key: string): key is typeof postOrgCreateHookFields[number] => - postOrgCreateHookFields.includes(key as typeof postOrgCreateHookFields[number]); +const isHookField = (key: string): key is (typeof postOrgCreateHookFields)[number] => + postOrgCreateHookFields.includes(key as (typeof postOrgCreateHookFields)[number]); export const emitPostOrgCreate = async (authFields: AuthFields): Promise => { await emitter.emit( diff --git a/src/util/jsonXmlTools.ts b/src/util/jsonXmlTools.ts index ba7c79cb63..52d02ec402 100644 --- a/src/util/jsonXmlTools.ts +++ b/src/util/jsonXmlTools.ts @@ -41,7 +41,8 @@ export const writeJSONasXML = async ({ return fs.writeFile(path, xml); }; -export const JsonAsXml = ({ json, type, options = standardOptions }: JSONasXML): string => jsToXml.parse(type, fixExistingDollarSign(json), options); +export const JsonAsXml = ({ json, type, options = standardOptions }: JSONasXML): string => + jsToXml.parse(type, fixExistingDollarSign(json), options); export const fixExistingDollarSign = (existing: WriteJSONasXMLInputs['json']): Record => { const existingCopy = { ...existing } as Record; diff --git a/src/webOAuthServer.ts b/src/webOAuthServer.ts index 9345073875..b7633eb28c 100644 --- a/src/webOAuthServer.ts +++ b/src/webOAuthServer.ts @@ -12,7 +12,7 @@ import { parse as parseQueryString } from 'querystring'; import { parse as parseUrl } from 'url'; import { Socket } from 'net'; import { EventEmitter } from 'events'; -import { JwtOAuth2Config, OAuth2 } from 'jsforce'; +import { OAuth2 } from 'jsforce'; import { AsyncCreatable, Env, set, toNumber } from '@salesforce/kit'; import { asString, get, Nullable } from '@salesforce/ts-types'; import { Logger } from './logger'; @@ -20,6 +20,7 @@ import { AuthInfo, DEFAULT_CONNECTED_APP_INFO } from './org'; import { SfError } from './sfError'; import { Messages } from './messages'; import { SfProjectJson } from './sfProject'; +import { JwtOAuth2Config } from './org/authInfo'; Messages.importMessagesDirectory(__dirname); const messages = Messages.loadMessages('@salesforce/core', 'auth'); diff --git a/test/unit/org/authInfoTest.ts b/test/unit/org/authInfoTest.ts index 5a027cdfa7..a205b3316b 100644 --- a/test/unit/org/authInfoTest.ts +++ b/test/unit/org/authInfoTest.ts @@ -18,8 +18,9 @@ import { AnyJson, getJsonMap, JsonMap, toJsonMap } from '@salesforce/ts-types'; import { expect } from 'chai'; import { Transport } from 'jsforce/lib/transport'; -import { JwtOAuth2Config, OAuth2 } from 'jsforce'; +import { OAuth2 } from 'jsforce'; import { SinonSpy, SinonStub } from 'sinon'; +import { JwtOAuth2Config } from '../../../src/org/authInfo'; import { AuthFields, AuthInfo } from '../../../src/org'; import { MockTestOrgData, shouldThrow, shouldThrowSync, TestContext } from '../../../src/testSetup'; import { OrgConfigProperties } from '../../../src/org/orgConfigProperties'; diff --git a/yarn.lock b/yarn.lock index 3de94da7a6..6b7960f2f9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2976,10 +2976,10 @@ jsesc@^2.5.1: resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== -jsforce@^2.0.0-beta.24: - version "2.0.0-beta.24" - resolved "https://registry.yarnpkg.com/jsforce/-/jsforce-2.0.0-beta.24.tgz#fe054eb0f6f668eff10566e086892dd2387364f7" - integrity sha512-rbDC9Y054Ele3qlDyFZxFY6RRyqpH7DKPYhAwBM2TIzqOl9OG35EB4lnJLaIuv/MZVA2mvTIV/TwxVv8PiB1EA== +jsforce@^2.0.0-beta.25: + version "2.0.0-beta.25" + resolved "https://registry.yarnpkg.com/jsforce/-/jsforce-2.0.0-beta.25.tgz#120a3999babf96ae18ab8f1232003d56588a9ca5" + integrity sha512-ZtzwJErI4SSJYWrGAw0mHEHPZRB4Idz0RiXHakCtEgEjEWt6JIDR4sNbWRHUzWHdEO4O61z2YSBvdOuag1hkWg== dependencies: "@babel/runtime" "^7.12.5" "@babel/runtime-corejs3" "^7.12.5" From 7b6565b2f36322be85e6c9ff3dbb51844279c8f5 Mon Sep 17 00:00:00 2001 From: mshanemc Date: Tue, 27 Jun 2023 13:16:18 -0500 Subject: [PATCH 02/37] feat: pino logger --- package.json | 5 +- src/config/config.ts | 2 +- src/config/configFile.ts | 2 +- src/crypto/crypto.ts | 2 +- src/crypto/keyChain.ts | 2 +- src/deviceOauthService.ts | 2 +- src/exported.ts | 2 +- src/logger.ts | 945 ------------------- src/logger/filters.ts | 79 ++ src/logger/logger.ts | 674 +++++++++++++ src/logger/memoryLogger.ts | 27 + src/logger/transformStream.ts | 64 ++ src/org/authInfo.ts | 2 +- src/org/authRemover.ts | 2 +- src/org/connection.ts | 2 +- src/org/org.ts | 2 +- src/org/permissionSetAssignment.ts | 2 +- src/org/scratchOrgCreate.ts | 2 +- src/org/scratchOrgErrorCodes.ts | 2 +- src/org/scratchOrgInfoApi.ts | 2 +- src/org/scratchOrgSettingsGenerator.ts | 2 +- src/org/user.ts | 2 +- src/schema/printer.ts | 2 +- src/schema/validator.ts | 2 +- src/stateAggregator/accessors/orgAccessor.ts | 2 +- src/status/myDomainResolver.ts | 2 +- src/status/pollingClient.ts | 2 +- src/status/streamingClient.ts | 2 +- src/testSetup.ts | 6 +- src/util/sfdc.ts | 6 +- src/util/sfdcUrl.ts | 2 +- src/util/unwrapArray.ts | 12 + src/util/zipWriter.ts | 2 +- src/webOAuthServer.ts | 2 +- test/unit/logger/filterTest.ts | 111 +++ test/unit/loggerTest.ts | 308 +----- yarn.lock | 292 ++++-- 37 files changed, 1219 insertions(+), 1360 deletions(-) delete mode 100644 src/logger.ts create mode 100644 src/logger/filters.ts create mode 100644 src/logger/logger.ts create mode 100644 src/logger/memoryLogger.ts create mode 100644 src/logger/transformStream.ts create mode 100644 src/util/unwrapArray.ts create mode 100644 test/unit/logger/filterTest.ts diff --git a/package.json b/package.json index a5788010f0..6affc21f13 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,6 @@ "messageTransformer/messageTransformer.ts" ], "dependencies": { - "@salesforce/bunyan": "^2.0.0", "@salesforce/kit": "^3.0.3", "@salesforce/schemas": "^1.5.1", "@salesforce/ts-types": "^2.0.2", @@ -52,6 +51,8 @@ "jsforce": "^2.0.0-beta.25", "jsonwebtoken": "9.0.0", "jszip": "3.10.1", + "pino": "^8.14.1", + "pino-pretty": "^10.0.0", "proper-lockfile": "^4.1.2", "ts-retry-promise": "^0.7.0" }, @@ -171,4 +172,4 @@ ] } } -} \ No newline at end of file +} diff --git a/src/config/config.ts b/src/config/config.ts index e92a550395..99cc21c69c 100644 --- a/src/config/config.ts +++ b/src/config/config.ts @@ -10,7 +10,7 @@ import * as fs from 'fs'; import { keyBy, parseJsonMap, set } from '@salesforce/kit'; import { Dictionary, ensure, isString, JsonPrimitive, Nullable } from '@salesforce/ts-types'; import { Global } from '../global'; -import { Logger } from '../logger'; +import { Logger } from '../logger/logger'; import { Messages } from '../messages'; import { validateApiVersion } from '../util/sfdc'; import { SfdcUrl } from '../util/sfdcUrl'; diff --git a/src/config/configFile.ts b/src/config/configFile.ts index 8dfbcba8f0..1bb38b45d6 100644 --- a/src/config/configFile.ts +++ b/src/config/configFile.ts @@ -12,7 +12,7 @@ import { dirname as pathDirname, join as pathJoin } from 'path'; import { isPlainObject } from '@salesforce/ts-types'; import { parseJsonMap } from '@salesforce/kit'; import { Global } from '../global'; -import { Logger } from '../logger'; +import { Logger } from '../logger/logger'; import { SfError } from '../sfError'; import { resolveProjectPath, resolveProjectPathSync } from '../util/internal'; import { BaseConfigStore, ConfigContents } from './configStore'; diff --git a/src/crypto/crypto.ts b/src/crypto/crypto.ts index 9ae02d26f4..880a698bbe 100644 --- a/src/crypto/crypto.ts +++ b/src/crypto/crypto.ts @@ -11,7 +11,7 @@ import * as os from 'os'; import { join as pathJoin } from 'path'; import { ensure, Nullable, Optional } from '@salesforce/ts-types'; import { AsyncOptionalCreatable, env } from '@salesforce/kit'; -import { Logger } from '../logger'; +import { Logger } from '../logger/logger'; import { Messages } from '../messages'; import { Cache } from '../util/cache'; import { Global } from '../global'; diff --git a/src/crypto/keyChain.ts b/src/crypto/keyChain.ts index e0c70cd6df..ffe8806c3d 100644 --- a/src/crypto/keyChain.ts +++ b/src/crypto/keyChain.ts @@ -6,7 +6,7 @@ */ import { env } from '@salesforce/kit'; -import { Logger } from '../logger'; +import { Logger } from '../logger/logger'; import { Messages } from '../messages'; import { KeyChain, keyChainImpl } from './keyChainImpl'; diff --git a/src/deviceOauthService.ts b/src/deviceOauthService.ts index 815523a426..e565c64620 100644 --- a/src/deviceOauthService.ts +++ b/src/deviceOauthService.ts @@ -12,7 +12,7 @@ import { AsyncCreatable, Duration, parseJsonMap } from '@salesforce/kit'; import { HttpRequest, OAuth2Config } from 'jsforce'; import { ensureString, JsonMap, Nullable } from '@salesforce/ts-types'; import * as FormData from 'form-data'; -import { Logger } from './logger'; +import { Logger } from './logger/logger'; import { AuthInfo, DEFAULT_CONNECTED_APP_INFO, SFDX_HTTP_HEADERS } from './org'; import { SfError } from './sfError'; import { Messages } from './messages'; diff --git a/src/exported.ts b/src/exported.ts index 3a8751097f..2d59c53563 100644 --- a/src/exported.ts +++ b/src/exported.ts @@ -63,7 +63,7 @@ export { LoggerOptions, LoggerStream, Logger, -} from './logger'; +} from './logger/logger'; export { Messages, StructuredMessage } from './messages'; diff --git a/src/logger.ts b/src/logger.ts deleted file mode 100644 index 57dc8e4b0e..0000000000 --- a/src/logger.ts +++ /dev/null @@ -1,945 +0,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 - */ -import { EventEmitter } from 'events'; -import * as os from 'os'; -import * as path from 'path'; -import { Writable } from 'stream'; -import * as fs from 'fs'; -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-ignore -import * as Bunyan from '@salesforce/bunyan'; -import { Env, parseJson, parseJsonMap } from '@salesforce/kit'; -import { - Dictionary, - ensure, - ensureNumber, - isArray, - isFunction, - isKeyOf, - isObject, - isPlainObject, - isString, - Many, - Optional, -} from '@salesforce/ts-types'; -import * as Debug from 'debug'; -import { Global, Mode } from './global'; -import { SfError } from './sfError'; - -/* eslint-disable @typescript-eslint/no-unsafe-return */ -/* eslint-disable @typescript-eslint/no-unsafe-member-access */ -/* eslint-disable @typescript-eslint/no-unsafe-assignment */ -/* eslint-disable @typescript-eslint/no-unsafe-call */ -/* eslint-disable @typescript-eslint/explicit-function-return-type */ -/* eslint-disable @typescript-eslint/restrict-template-expressions */ - -/** - * A Bunyan `Serializer` function. - * - * @param input The input to be serialized. - * **See** {@link https://github.com/forcedotcom/node-bunyan#serializers|Bunyan Serializers API} - */ -export type Serializer = (input: unknown) => unknown; - -/** - * A collection of named `Serializer`s. - * - * **See** {@link https://github.com/forcedotcom/node-bunyan#serializers|Bunyan Serializers API} - */ -export interface Serializers { - [key: string]: Serializer; -} - -/** - * The common set of `Logger` options. - */ -export interface LoggerOptions { - /** - * The logger name. - */ - name: string; - - /** - * The logger format type. Current options include LogFmt or JSON (default). - */ - format?: LoggerFormat; - - /** - * The logger's serializers. - */ - serializers?: Serializers; - /** - * Whether or not to log source file, line, and function information. - */ - src?: boolean; - /** - * The desired log level. - */ - level?: LoggerLevelValue; - /** - * A stream to write to. - */ - stream?: Writable; - /** - * An array of streams to write to. - */ - streams?: LoggerStream[]; -} - -/** - * Standard `Logger` levels. - * - * **See** {@link https://github.com/forcedotcom/node-bunyan#levels|Bunyan Levels} - */ -export enum LoggerLevel { - TRACE = 10, - DEBUG = 20, - INFO = 30, - WARN = 40, - ERROR = 50, - FATAL = 60, -} - -/** - * `Logger` format types. - */ -export enum LoggerFormat { - JSON, - LOGFMT, -} - -/** - * A Bunyan stream configuration. - * - * @see {@link https://github.com/forcedotcom/node-bunyan#streams|Bunyan Streams} - */ -export interface LoggerStream { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - [key: string]: any; - /** - * The type of stream -- may be inferred from other properties. - */ - type?: string; - /** - * The desired log level for the stream. - */ - level?: LoggerLevelValue; - /** - * The stream to write to. Mutually exclusive with `path`. - */ - stream?: Writable; - /** - * The name of the stream. - */ - name?: string; - /** - * A log file path to write to. Mutually exclusive with `stream`. - */ - path?: string; -} - -/** - * Any numeric `Logger` level. - */ -export type LoggerLevelValue = LoggerLevel | number; - -/** - * A collection of named `FieldValue`s. - * - * **See** {@link https://github.com/forcedotcom/node-bunyan#log-record-fields|Bunyan Log Record Fields} - */ -export interface Fields { - [key: string]: FieldValue; -} - -/** - * All possible field value types. - */ -export type FieldValue = string | number | boolean; - -/** - * Log line interface - */ -export interface LogLine { - name: string; - hostname: string; - pid: string; - log: string; - level: number; - msg: string; - time: string; - v: number; -} - -/** - * A logging abstraction powered by {@link https://github.com/forcedotcom/node-bunyan|Bunyan} that provides both a default - * logger configuration that will log to `sfdx.log`, 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'}); - * ``` - * **See** https://github.com/forcedotcom/node-bunyan - * - * **See** https://developer.salesforce.com/docs/atlas.en-us.sfdx_setup.meta/sfdx_setup/sfdx_dev_cli_log_messages.htm - */ -export class Logger { - /** - * The name of the root sfdx `Logger`. - */ - public static readonly ROOT_NAME = 'sf'; - - /** - * The default `LoggerLevel` when constructing new `Logger` instances. - */ - public static readonly DEFAULT_LEVEL = LoggerLevel.WARN; - - /** - * A list of all lower case `LoggerLevel` names. - * - * **See** {@link LoggerLevel} - */ - public static readonly LEVEL_NAMES = Object.values(LoggerLevel) - .filter(isString) - .map((v: string) => v.toLowerCase()); - // Rollup all instance-specific process event listeners together to prevent global `MaxListenersExceededWarning`s. - private static readonly lifecycle = ((): EventEmitter => { - const events = new EventEmitter(); - events.setMaxListeners(0); // never warn on listener counts - process.on('uncaughtException', (err) => events.emit('uncaughtException', err)); - process.on('exit', () => events.emit('exit')); - return events; - })(); - - // The sfdx root logger singleton - private static rootLogger?: Logger; - - /** - * The default rotation period for logs. Example '1d' will rotate logs daily (at midnight). - * See 'period' docs here: https://github.com/forcedotcom/node-bunyan#stream-type-rotating-file - */ - - public readonly logRotationPeriod = new Env().getString('SF_LOG_ROTATION_PERIOD') ?? '1d'; - - /** - * The number of backup rotated log files to keep. - * Example: '3' will have the base sf.log file, and the past 3 (period) log files. - * See 'count' docs here: https://github.com/forcedotcom/node-bunyan#stream-type-rotating-file - */ - - public readonly logRotationCount = new Env().getNumber('SF_LOG_ROTATION_COUNT') ?? 2; - - /** - * Whether debug is enabled for this Logger. - */ - public debugEnabled = false; - - // The actual Bunyan logger - private bunyan: Bunyan; - - private readonly format: LoggerFormat; - - /** - * 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`. - */ - public constructor(optionsOrName: LoggerOptions | string) { - let options: LoggerOptions; - if (typeof optionsOrName === 'string') { - options = { - name: optionsOrName, - level: Logger.DEFAULT_LEVEL, - serializers: Bunyan.stdSerializers, - }; - } else { - options = optionsOrName; - } - - if (Logger.rootLogger && options.name === Logger.ROOT_NAME) { - throw new SfError('Can not create another root logger.', 'RedundantRootLoggerError'); - } - - // Inspect format to know what logging format to use then delete from options to - // ensure it doesn't conflict with Bunyan. - this.format = options.format ?? LoggerFormat.JSON; - delete options.format; - - // If the log format is LOGFMT, we need to convert any stream(s) into a LOGFMT type stream. - if (this.format === LoggerFormat.LOGFMT && options.stream) { - const ls: LoggerStream = this.createLogFmtFormatterStream({ stream: options.stream }); - options.stream = ls.stream; - } - if (this.format === LoggerFormat.LOGFMT && options.streams) { - const logFmtConvertedStreams: LoggerStream[] = []; - options.streams.forEach((ls: LoggerStream) => { - logFmtConvertedStreams.push(this.createLogFmtFormatterStream(ls)); - }); - options.streams = logFmtConvertedStreams; - } - - this.bunyan = new Bunyan(options); - this.bunyan.name = options.name; - this.bunyan.filters = []; - - if (!options.streams && !options.stream) { - this.bunyan.streams = []; - } - - // all SFDX loggers must filter sensitive data - this.addFilter((...args) => filterSecrets(...args)); - - if (Global.getEnvironmentMode() !== Mode.TEST) { - Logger.lifecycle.on('uncaughtException', this.uncaughtExceptionHandler); - Logger.lifecycle.on('exit', this.exitHandler); - } - - this.trace(`Created '${this.getName()}' logger instance`); - } - - /** - * Gets the root logger with the default level, file stream, and DEBUG enabled. - */ - public static async root(): Promise { - if (this.rootLogger) { - return this.rootLogger; - } - const rootLogger = (this.rootLogger = new Logger(Logger.ROOT_NAME).setLevel()); - - // disable log file writing, if applicable - const disableLogFile = new Env().getString('SF_DISABLE_LOG_FILE'); - if (disableLogFile !== 'true' && Global.getEnvironmentMode() !== Mode.TEST) { - await rootLogger.addLogFileStream(Global.LOG_FILE_PATH); - } - - rootLogger.enableDEBUG(); - return rootLogger; - } - - /** - * Gets the root logger with the default level, file stream, and DEBUG enabled. - */ - public static getRoot(): Logger { - if (this.rootLogger) { - return this.rootLogger; - } - const rootLogger = (this.rootLogger = new Logger(Logger.ROOT_NAME).setLevel()); - - // disable log file writing, if applicable - if (process.env.SFDX_DISABLE_LOG_FILE !== 'true' && Global.getEnvironmentMode() !== Mode.TEST) { - rootLogger.addLogFileStreamSync(Global.LOG_FILE_PATH); - } - - rootLogger.enableDEBUG(); - return rootLogger; - } - - /** - * Destroys the root `Logger`. - * - * @ignore - */ - public static destroyRoot(): void { - if (this.rootLogger) { - this.rootLogger.close(); - this.rootLogger = undefined; - } - } - - /** - * Create a child of the root logger, inheriting this instance's configuration such as `level`, `streams`, etc. - * - * @param name The name of the child logger. - * @param fields Additional fields included in all log lines. - */ - public static async child(name: string, fields?: Fields): Promise { - return (await Logger.root()).child(name, fields); - } - - /** - * Create a child of the root logger, inheriting this instance's configuration such as `level`, `streams`, etc. - * - * @param name The name of the child logger. - * @param fields Additional fields included in all log lines. - */ - public static childFromRoot(name: string, fields?: Fields): Logger { - 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} - */ - public static getLevelByName(levelName: string): LoggerLevelValue { - levelName = levelName.toUpperCase(); - if (!isKeyOf(LoggerLevel, levelName)) { - throw new SfError(`Invalid log level "${levelName}".`, 'UnrecognizedLoggerLevelNameError'); - } - return LoggerLevel[levelName]; - } - - /** - * Adds a stream. - * - * @param stream The stream configuration to add. - * @param defaultLevel The default level of the stream. - */ - public addStream(stream: LoggerStream, defaultLevel?: LoggerLevelValue): void { - if (this.format === LoggerFormat.LOGFMT) { - stream = this.createLogFmtFormatterStream(stream); - } - this.bunyan.addStream(stream, defaultLevel); - } - - /** - * Adds a file stream to this logger. Resolved or rejected upon completion of the addition. - * - * @param logFile The path to the log file. If it doesn't exist it will be created. - */ - public async addLogFileStream(logFile: string): Promise { - try { - // Check if we have write access to the log file (i.e., we created it already) - await fs.promises.access(logFile, fs.constants.W_OK); - } catch (err1) { - try { - if (process.platform === 'win32') { - await fs.promises.mkdir(path.dirname(logFile), { recursive: true }); - } else { - await fs.promises.mkdir(path.dirname(logFile), { recursive: true, mode: 0o700 }); - } - } catch (err2) { - throw SfError.wrap(err2 as string | Error); - } - try { - await fs.promises.writeFile(logFile, '', { mode: '600' }); - } catch (err3) { - throw SfError.wrap(err3 as string | Error); - } - } - - // avoid multiple streams to same log file - if ( - !this.bunyan.streams.find( - // No bunyan typings - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (stream: any) => stream.type === 'rotating-file' && stream.path === logFile - ) - ) { - this.addStream({ - type: 'rotating-file', - path: logFile, - period: this.logRotationPeriod, - count: this.logRotationCount, - level: this.bunyan.level(), - }); - } - } - - /** - * Adds a file stream to this logger. Resolved or rejected upon completion of the addition. - * - * @param logFile The path to the log file. If it doesn't exist it will be created. - */ - public addLogFileStreamSync(logFile: string): void { - try { - // Check if we have write access to the log file (i.e., we created it already) - fs.accessSync(logFile, fs.constants.W_OK); - } catch (err1) { - try { - if (process.platform === 'win32') { - fs.mkdirSync(path.dirname(logFile), { recursive: true }); - } else { - fs.mkdirSync(path.dirname(logFile), { recursive: true, mode: 0o700 }); - } - } catch (err2) { - throw SfError.wrap(err2 as Error); - } - try { - fs.writeFileSync(logFile, '', { mode: '600' }); - } catch (err3) { - throw SfError.wrap(err3 as string | Error); - } - } - - // avoid multiple streams to same log file - if ( - !this.bunyan.streams.find( - // No bunyan typings - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (stream: any) => stream.type === 'rotating-file' && stream.path === logFile - ) - ) { - this.addStream({ - type: 'rotating-file', - path: logFile, - period: this.logRotationPeriod, - count: this.logRotationCount, - level: this.bunyan.level(), - }); - } - } - - /** - * Gets the name of this logger. - */ - public getName(): string { - return this.bunyan.name; - } - - /** - * Gets the current level of this logger. - */ - public getLevel(): LoggerLevelValue { - return this.bunyan.level(); - } - - /** - * 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')) - * ``` - */ - public setLevel(level?: LoggerLevelValue): Logger { - if (level == null) { - const logLevelFromEnvVar = new Env().getString('SF_LOG_LEVEL'); - level = logLevelFromEnvVar ? Logger.getLevelByName(logLevelFromEnvVar) : Logger.DEFAULT_LEVEL; - } - this.bunyan.level(level); - return this; - } - - /** - * Gets the underlying Bunyan logger. - */ - // leave this typed as any to keep if from trying to export the type from the untyped bunyan module - // this prevents consumers from getting node_modules/@salesforce/core/lib/logger.d.ts:281:24 - error TS2304: Cannot find name 'Bunyan'. - // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any - public getBunyanLogger(): any { - return this.bunyan; - } - - /** - * 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. - */ - public shouldLog(level: LoggerLevelValue): boolean { - if (typeof level === 'string') { - level = Bunyan.levelFromName(level) as number; - } - return level >= this.getLevel(); - } - - /** - * Use in-memory logging for this logger instance instead of any parent streams. Useful for testing. - * For convenience this object is returned. - * - * **WARNING: This cannot be undone for this logger instance.** - */ - public useMemoryLogging(): Logger { - this.bunyan.streams = []; - this.bunyan.ringBuffer = new Bunyan.RingBuffer({ limit: 5000 }); - this.addStream({ - type: 'raw', - stream: this.bunyan.ringBuffer, - level: this.bunyan.level(), - }); - return this; - } - - /** - * Gets an array of log line objects. Each element is an object that corresponds to a log line. - */ - public getBufferedRecords(): LogLine[] { - if (this.bunyan.ringBuffer) { - return this.bunyan.ringBuffer.records; - } - return []; - } - - /** - * Reads a text blob of all the log lines contained in memory or the log file. - */ - public readLogContentsAsText(): string { - if (this.bunyan.ringBuffer) { - return this.getBufferedRecords().reduce((accum, line) => { - accum += JSON.stringify(line) + os.EOL; - return accum; - }, ''); - } else { - let content = ''; - // No bunyan typings - // eslint-disable-next-line @typescript-eslint/no-explicit-any - this.bunyan.streams.forEach(async (stream: any) => { - if (stream.type === 'file') { - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - content += await fs.promises.readFile(stream.path, 'utf8'); - } - }); - return content; - } - } - - /** - * Adds a filter to be applied to all logged messages. - * - * @param filter A function with signature `(...args: any[]) => any[]` that transforms log message arguments. - */ - public addFilter(filter: (...args: unknown[]) => unknown): void { - if (!this.bunyan.filters) { - this.bunyan.filters = []; - } - this.bunyan.filters.push(filter); - } - - /** - * Close the logger, including any streams, and remove all listeners. - * - * @param fn A function with signature `(stream: LoggerStream) => void` to call for each stream with the stream as an arg. - */ - public close(fn?: (stream: LoggerStream) => void): void { - if (this.bunyan.streams) { - try { - this.bunyan.streams.forEach((entry: LoggerStream) => { - if (fn) { - fn(entry); - } - // close file streams, flush buffer to disk - // eslint-disable-next-line @typescript-eslint/unbound-method - if (entry.type === 'file' && entry.stream && isFunction(entry.stream.end)) { - entry.stream.end(); - } - }); - } finally { - Logger.lifecycle.removeListener('uncaughtException', this.uncaughtExceptionHandler); - Logger.lifecycle.removeListener('exit', this.exitHandler); - } - } - } - - /** - * 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 as `log:`. - * @param fields Additional fields included in all log lines for the child logger. - */ - public child(name: string, fields: Fields = {}): Logger { - if (!name) { - throw new SfError('LoggerNameRequired'); - } - fields.log = name; - - const child = new Logger(name); - // only support including additional fields on log line (no config) - child.bunyan = this.bunyan.child(fields, true); - child.bunyan.name = name; - child.bunyan.filters = this.bunyan.filters; - - this.trace(`Setup child '${name}' 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. - */ - public addField(name: string, value: FieldValue): Logger { - this.bunyan.fields[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 - public trace(...args: any[]): Logger { - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - this.bunyan.trace(this.applyFilters(LoggerLevel.TRACE, ...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. - */ - public debug(...args: unknown[]): Logger { - this.bunyan.debug(this.applyFilters(LoggerLevel.DEBUG, ...args)); - return this; - } - - /** - * Logs at `debug` level with filtering applied. - * - * @param cb A callback that returns on array objects to be logged. - */ - public debugCallback(cb: () => unknown[] | string): void { - if (this.getLevel() === LoggerLevel.DEBUG || process.env.DEBUG) { - const result = cb(); - if (isArray(result)) { - this.bunyan.debug(this.applyFilters(LoggerLevel.DEBUG, ...result)); - } else { - this.bunyan.debug(this.applyFilters(LoggerLevel.DEBUG, ...[result])); - } - } - } - - /** - * Logs at `info` level with filtering applied. For convenience `this` object is returned. - * - * @param args Any number of arguments to be logged. - */ - public info(...args: unknown[]): Logger { - this.bunyan.info(this.applyFilters(LoggerLevel.INFO, ...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. - */ - public warn(...args: unknown[]): Logger { - this.bunyan.warn(this.applyFilters(LoggerLevel.WARN, ...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. - */ - public error(...args: unknown[]): Logger { - this.bunyan.error(this.applyFilters(LoggerLevel.ERROR, ...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. - */ - public fatal(...args: unknown[]): Logger { - // 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.bunyan.fatal(this.applyFilters(LoggerLevel.FATAL, ...args)); - return this; - } - - /** - * Enables logging to stdout when the DEBUG environment variable is used. It uses the logger - * name as the debug name, so you can do DEBUG= to filter the results to your logger. - */ - public enableDEBUG(): void { - // The debug library does this for you, but no point setting up the stream if it isn't there - if (process.env.DEBUG && !this.debugEnabled) { - const debuggers: Dictionary = {}; - - debuggers.core = Debug(`${this.getName()}:core`); - - this.addStream({ - name: 'debug', - stream: new Writable({ - write: (chunk, encoding, next) => { - try { - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - const json = parseJsonMap(chunk.toString()); - const logLevel = ensureNumber(json.level); - if (this.getLevel() <= logLevel) { - let debuggerName = 'core'; - if (isString(json.log)) { - debuggerName = json.log; - if (!debuggers[debuggerName]) { - debuggers[debuggerName] = Debug(`${this.getName()}:${debuggerName}`); - } - } - const level = LoggerLevel[logLevel]; - ensure(debuggers[debuggerName])(`${level} ${json.msg}`); - } - } catch (err) { - // do nothing - } - next(); - }, - }), - // Consume all levels - level: 0, - }); - this.debugEnabled = true; - } - } - - private applyFilters(logLevel: LoggerLevel, ...args: unknown[]): Optional> { - if (this.shouldLog(logLevel)) { - // No bunyan typings - // eslint-disable-next-line @typescript-eslint/no-explicit-any - this.bunyan.filters.forEach((filter: any) => (args = filter(...args))); - } - return args && args.length === 1 ? args[0] : args; - } - - private uncaughtExceptionHandler = (err: Error) => { - // W-7558552 - // Only log uncaught exceptions in root logger - if (this === Logger.rootLogger) { - // log the exception - // FIXME: good chance this won't be logged because - // process.exit was called before this is logged - // https://github.com/trentm/node-bunyan/issues/95 - this.fatal(err); - } - }; - - private exitHandler = () => { - this.close(); - }; - - // eslint-disable-next-line class-methods-use-this - private createLogFmtFormatterStream(loggerStream: LoggerStream): LoggerStream { - const logFmtWriteableStream = new Writable({ - write: (chunk, enc, cb) => { - try { - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - const parsedJSON = JSON.parse(chunk.toString()); - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - const keys = Object.keys(parsedJSON); - - let logEntry = ''; - keys.forEach((key) => { - let logMsg = `${parsedJSON[key]}`; - if (logMsg.trim().includes(' ')) { - logMsg = `"${logMsg}"`; - } - logEntry += `${key}=${logMsg} `; - }); - if (loggerStream.stream) { - loggerStream.stream.write(logEntry.trimRight() + '\n'); - } - } catch (error) { - if (loggerStream.stream) { - loggerStream.stream.write(chunk.toString()); - } - } - cb(null); - }, - }); - - return Object.assign({}, loggerStream, { stream: logFmtWriteableStream }); - } -} - -type FilteredKey = string | { name: string; regex: string }; - -// Ok to log clientid -const FILTERED_KEYS: FilteredKey[] = [ - 'sid', - 'Authorization', - // Any json attribute that contains the words "access" and "token" will have the attribute/value hidden - { name: 'access_token', regex: 'access[^\'"]*token' }, - // Any json attribute that contains the words "refresh" and "token" will have the attribute/value hidden - { name: 'refresh_token', regex: 'refresh[^\'"]*token' }, - 'clientsecret', - // Any json attribute that contains the words "sfdx", "auth", and "url" will have the attribute/value hidden - { name: 'sfdxauthurl', regex: 'sfdx[^\'"]*auth[^\'"]*url' }, -]; - -// SFDX code and plugins should never show tokens or connect app information in the logs -const filterSecrets = (...args: unknown[]): unknown => - args.map((arg) => { - if (isArray(arg)) { - return filterSecrets(...arg); - } - - if (arg) { - let mutableArg: string; - - // Normalize all objects into a string. This includes errors. - if (arg instanceof Buffer) { - mutableArg = ''; - } else if (isObject(arg)) { - mutableArg = JSON.stringify(arg); - } else if (isString(arg)) { - mutableArg = arg; - } else { - mutableArg = ''; - } - - const HIDDEN = 'HIDDEN'; - - FILTERED_KEYS.forEach((key: FilteredKey) => { - let expElement = key; - let expName = key; - - // Filtered keys can be strings or objects containing regular expression components. - if (isPlainObject(key)) { - expElement = key.regex; - expName = key.name; - } - - const hiddenAttrMessage = `"<${expName} - ${HIDDEN}>"`; - - // Match all json attribute values case insensitive: ex. {" Access*^&(*()^* Token " : " 45143075913458901348905 \n\t" ...} - const regexTokens = new RegExp(`(['"][^'"]*${expElement}[^'"]*['"]\\s*:\\s*)['"][^'"]*['"]`, 'gi'); - - mutableArg = mutableArg.replace(regexTokens, `$1${hiddenAttrMessage}`); - - // Match all key value attribute case insensitive: ex. {" key\t" : ' access_token ' , " value " : " dsafgasr431 " ....} - const keyRegex = new RegExp( - `(['"]\\s*key\\s*['"]\\s*:)\\s*['"]\\s*${expElement}\\s*['"]\\s*.\\s*['"]\\s*value\\s*['"]\\s*:\\s*['"]\\s*[^'"]*['"]`, - 'gi' - ); - mutableArg = mutableArg.replace(keyRegex, `$1${hiddenAttrMessage}`); - }); - - mutableArg = mutableArg.replace(/(00D\w{12,15})![.\w]*/, `<${HIDDEN}>`); - - // return an object if an object was logged; otherwise return the filtered string. - return isObject(arg) ? parseJson(mutableArg) : mutableArg; - } else { - return arg; - } - }); diff --git a/src/logger/filters.ts b/src/logger/filters.ts new file mode 100644 index 0000000000..48d456bc36 --- /dev/null +++ b/src/logger/filters.ts @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2023, 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 + */ +import { parseJson } from '@salesforce/kit'; +import { isArray, isObject, isString } from '@salesforce/ts-types'; +import { accessTokenRegex } from '../exported'; + +type FilteredKey = string | { name: string; regex: string }; +// Ok to log clientid +const FILTERED_KEYS: FilteredKey[] = [ + 'sid', + 'Authorization', + // Any json attribute that contains the words "access" and "token" will have the attribute/value hidden + { name: 'access_token', regex: 'access[^\'"]*token' }, + // Any json attribute that contains the words "refresh" and "token" will have the attribute/value hidden + { name: 'refresh_token', regex: 'refresh[^\'"]*token' }, + 'clientsecret', + // Any json attribute that contains the words "sfdx", "auth", and "url" will have the attribute/value hidden + { name: 'sfdxauthurl', regex: 'sfdx[^\'"]*auth[^\'"]*url' }, +]; +// SFDX code and plugins should never show tokens or connect app information in the logs + +export const HIDDEN = 'HIDDEN'; +/** + * + * @param args you *probably are passing this an object, but it can handle any type + * @returns + */ +export const filterSecrets = (...args: unknown[]): unknown => + args.map((arg) => { + if (isArray(arg)) { + return filterSecrets(...arg); + } + + if (arg) { + let mutableArg: string; + + // Normalize all objects into a string. This includes errors. + if (arg instanceof Buffer) { + mutableArg = ''; + } else if (isObject(arg)) { + mutableArg = JSON.stringify(arg); + } else if (isString(arg)) { + mutableArg = arg; + } else { + mutableArg = ''; + } + + FILTERED_KEYS.forEach((key: FilteredKey) => { + // Filtered keys can be strings or objects containing regular expression components. + const expElement = typeof key === 'string' ? key : key.regex; + const expName = typeof key === 'string' ? key : key.name; + + const hiddenAttrMessage = `"<${expName} - ${HIDDEN}>"`; + + // Match all json attribute values case insensitive: ex. {" Access*^&(*()^* Token " : " 45143075913458901348905 \n\t" ...} + const regexTokens = new RegExp(`(['"][^'"]*${expElement}[^'"]*['"]\\s*:\\s*)['"][^'"]*['"]`, 'gi'); + + mutableArg = mutableArg.replace(regexTokens, `$1${hiddenAttrMessage}`); + + // Match all key value attribute case insensitive: ex. {" key\t" : ' access_token ' , " value " : " dsafgasr431 " ....} + const keyRegex = new RegExp( + `(['"]\\s*key\\s*['"]\\s*:)\\s*['"]\\s*${expElement}\\s*['"]\\s*.\\s*['"]\\s*value\\s*['"]\\s*:\\s*['"]\\s*[^'"]*['"]`, + 'gi' + ); + mutableArg = mutableArg.replace(keyRegex, `$1${hiddenAttrMessage}`); + }); + + mutableArg = mutableArg.replace(accessTokenRegex, `<${HIDDEN}>`); + + // return an object if an object was logged; otherwise return the filtered string. + return isObject(arg) ? parseJson(mutableArg) : mutableArg; + } else { + return arg; + } + }); diff --git a/src/logger/logger.ts b/src/logger/logger.ts new file mode 100644 index 0000000000..b632602609 --- /dev/null +++ b/src/logger/logger.ts @@ -0,0 +1,674 @@ +/* + * 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 + */ +import * as os from 'os'; +import * as path from 'path'; + +import { Writable } from 'stream'; +import { Logger as PinoLogger, pino } from 'pino'; +import { Env } from '@salesforce/kit'; +import { isKeyOf, isString } from '@salesforce/ts-types'; +import { Global, Mode } from '../global'; +import { SfError } from '../sfError'; +import { unwrapArray } from '../util/unwrapArray'; +import { MemoryLogger } from './memoryLogger'; + +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +/* eslint-disable @typescript-eslint/restrict-template-expressions */ + +/** + * @deprecated. Don't use serializers + * A Bunyan `Serializer` function. + * + * @param input The input to be serialized. + */ +export type Serializer = (input: unknown) => unknown; + +/** + * @deprecated. Don't use serializers + * A collection of named `Serializer`s. + * + */ +export interface Serializers { + [key: string]: Serializer; +} + +/** + * The common set of `Logger` options. + */ +export interface LoggerOptions { + /** + * The logger name. + */ + name: string; + + /** + * @deprecated. JSON is the only format. + * The logger format type. Current options include LogFmt or JSON (default). + */ + format?: LoggerFormat; + + /** + * The logger's serializers. + * + * @deprecated. Serializers are not configurable + */ + serializers?: Serializers; + /** + * Whether or not to log source file, line, and function information. + * + * @deprecated. This never did anything + */ + src?: boolean; + /** + * The desired log level. + */ + level?: LoggerLevelValue; + /** + * A stream to write to. + */ + stream?: Writable; + /** + * An array of streams to write to. + */ + streams?: LoggerStream[]; + + /** + * Create a logger with the fields set + */ + fields?: Fields; + + /** log to memory instead of to a file */ + useMemoryLogger?: boolean; +} + +/** + * Standard `Logger` levels. + * + * **See** {@link https://getpino.io/#/docs/api?id=logger-level |Logger Levels} + */ +export enum LoggerLevel { + TRACE = 10, + DEBUG = 20, + INFO = 30, + WARN = 40, + ERROR = 50, + FATAL = 60, +} + +/** + * `Logger` format types. + * + * @deprecated JSON is the only option + */ +export enum LoggerFormat { + JSON, + LOGFMT, +} + +/** + * A Bunyan stream configuration. + * + * @deprecated. Do not use + * + */ +export interface LoggerStream { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + [key: string]: any; + /** + * The type of stream -- may be inferred from other properties. + */ + type?: string; + /** + * The desired log level for the stream. + */ + level?: LoggerLevelValue; + /** + * The stream to write to. Mutually exclusive with `path`. + */ + stream?: Writable; + /** + * The name of the stream. + */ + name?: string; + /** + * A log file path to write to. Mutually exclusive with `stream`. + */ + path?: string; +} + +/** + * Any numeric `Logger` level. + */ +export type LoggerLevelValue = LoggerLevel | number; + +/** + * An object + */ +export type Fields = Record; + +/** + * All possible field value types. + * + * @deprecated. Fields can be anything, including another object + */ +export type FieldValue = string | number | boolean; + +/** + * Log line interface + */ +export interface LogLine { + name: string; + hostname: string; + pid: string; + log: string; + level: number; + msg: string; + time: string; + v: number; +} + +/** + * 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 + */ +export class Logger { + /** + * The name of the root sfdx `Logger`. + */ + public static readonly ROOT_NAME = 'sf'; + + /** + * The default `LoggerLevel` when constructing new `Logger` instances. + */ + public static readonly DEFAULT_LEVEL = LoggerLevel.WARN; + + /** + * A list of all lower case `LoggerLevel` names. + * + * **See** {@link LoggerLevel} + */ + public static readonly LEVEL_NAMES = Object.values(LoggerLevel) + .filter(isString) + .map((v: string) => v.toLowerCase()); + + // The sfdx root logger singleton + private static rootLogger?: Logger; + + /** + * @deprecated. Has no effect + */ + + public readonly logRotationCount = new Env().getNumber('SF_LOG_ROTATION_COUNT') ?? 2; + + /** + * Whether debug is enabled for this Logger. + */ + public debugEnabled = false; + + private pinoLogger: PinoLogger; + + private memoryLogger?: 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`. + */ + public constructor(optionsOrName: LoggerOptions | string) { + const options: LoggerOptions = + typeof optionsOrName === 'string' + ? { name: optionsOrName, level: Logger.DEFAULT_LEVEL, fields: {} } + : optionsOrName; + + if (Logger.rootLogger && options.name === Logger.ROOT_NAME) { + throw new SfError('Can not create another root logger.', 'RedundantRootLoggerError'); + } + + // if there is a rootLogger, use its Pino instance + if (Logger.rootLogger) { + // modify the name + this.pinoLogger = Logger.rootLogger.pinoLogger.child({ ...options.fields, name: options.name }); + this.memoryLogger = Logger.rootLogger.memoryLogger; + this.pinoLogger.trace(`Created '${options.name}' child logger instance`); + } else { + const level = new Env().getString('SF_LOG_LEVEL') ?? pino.levels.labels[options.level ?? Logger.DEFAULT_LEVEL]; + const commonOptions = { + name: options.name ?? Logger.ROOT_NAME, + base: options.fields ?? {}, + level, + enabled: process.env.SFDX_DISABLE_LOG_FILE !== 'true' && process.env.SF_DISABLE_LOG_FILE !== 'true', + }; + if (options.useMemoryLogger || Global.getEnvironmentMode() === Mode.TEST) { + this.memoryLogger = new MemoryLogger(); + this.pinoLogger = pino( + { + ...commonOptions, + sync: true, + }, + this.memoryLogger + ); + } else { + this.pinoLogger = pino({ + ...commonOptions, + transport: { + pipeline: [ + { + target: '../../lib/logger/transformStream', + }, + getWriteStream(level), + ], + }, + sync: false, + }); + } + + Logger.rootLogger = this; + } + } + + /** + * + * Gets the root logger with the default level, file stream, and DEBUG enabled. + * See also getRawLogger if you don't need the root logger + */ + public static async root(): Promise { + return Promise.resolve(this.getRoot()); + } + + /** + * Gets the root logger with the default level, file stream, and DEBUG enabled. + */ + public static getRoot(): Logger { + if (this.rootLogger) { + return this.rootLogger; + } + const rootLogger = (this.rootLogger = new Logger(Logger.ROOT_NAME)); + return rootLogger; + } + + /** + * Destroys the root `Logger`. + * + * @ignore + */ + public static destroyRoot(): void { + if (this.rootLogger) { + this.rootLogger = undefined; + } + } + + /** + * Create a child of the root logger, inheriting this instance's configuration such as `level`, `streams`, etc. + * + * @param name The name of the child logger. + * @param fields Additional fields included in all log lines. + */ + public static async child(name: string, fields?: Fields): Promise { + return (await Logger.root()).child(name, fields); + } + + /** + * Create a child of the root logger, inheriting this instance's configuration such as `level`, `streams`, etc. + * + * @param name The name of the child logger. + * @param fields Additional fields included in all log lines. + */ + public static childFromRoot(name: string, fields?: Fields): Logger { + 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} + */ + public static getLevelByName(levelName: string): LoggerLevelValue { + levelName = levelName.toUpperCase(); + if (!isKeyOf(LoggerLevel, levelName)) { + throw new SfError(`Invalid log level "${levelName}".`, 'UnrecognizedLoggerLevelNameError'); + } + return LoggerLevel[levelName]; + } + + /** get the bare (pino) logger instead of using the class hierarchy */ + public static getRawRootLogger(): PinoLogger { + return Logger.getRoot().pinoLogger; + } + + /** + * Adds a stream. + * + * @deprecated. addStream is a no-op + * + * @param stream The stream configuration to add. + * @param defaultLevel The default level of the stream. + */ + // eslint-disable-next-line class-methods-use-this, @typescript-eslint/no-empty-function, @typescript-eslint/no-unused-vars + public addStream(stream: LoggerStream, defaultLevel?: LoggerLevelValue): void {} + + /** + * Adds a file stream to this logger. Resolved or rejected upon completion of the addition. + * + * @deprecated. streams don't change once a logger is built + * @param logFile The path to the log file. If it doesn't exist it will be created. + */ + // eslint-disable-next-line class-methods-use-this, @typescript-eslint/no-empty-function, @typescript-eslint/no-unused-vars + public async addLogFileStream(logFile: string): Promise {} + + /** + * Adds a file stream to this logger. Resolved or rejected upon completion of the addition. + * + * @deprecated. streams don't change once a logger is built + + * @param logFile The path to the log file. If it doesn't exist it will be created. + */ + // eslint-disable-next-line class-methods-use-this, @typescript-eslint/no-empty-function, @typescript-eslint/no-unused-vars + public addLogFileStreamSync(logFile: string): void {} + + /** get the bare (pino) logger instead of using the class hierarchy */ + public getRawLogger(): PinoLogger { + return this.pinoLogger; + } + + /** + * Gets the name of this logger. + */ + public getName(): string { + return this.pinoLogger.bindings().name; + } + + /** + * Gets the current level of this logger. + */ + public getLevel(): LoggerLevelValue { + 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')) + * ``` + */ + public setLevel(level?: LoggerLevelValue): Logger { + if (level == null) { + const logLevelFromEnvVar = new Env().getString('SF_LOG_LEVEL'); + level = logLevelFromEnvVar ? Logger.getLevelByName(logLevelFromEnvVar) : Logger.DEFAULT_LEVEL; + } + this.pinoLogger.level = this.pinoLogger.levels.labels[level] ?? this.pinoLogger.levels.labels[Logger.DEFAULT_LEVEL]; + return this; + } + + /** + * Gets the underlying Bunyan logger. + * + * @deprecated. Bunyan is no longer used. This will return a pino Logger + */ + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any + public getBunyanLogger(): any { + return this.pinoLogger; + } + + /** + * 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. + */ + public shouldLog(level: LoggerLevelValue): boolean { + return (typeof level === 'string' ? this.pinoLogger.levelVal : level) >= this.getLevel(); + } + + /** + * @deprecated. Use the useMemoryLogging option when instantiating the logger + * Use in-memory logging for this logger instance instead of any parent streams. Useful for testing. + * For convenience this object is returned. + * + * **WARNING: This cannot be undone for this logger instance.** + */ + public useMemoryLogging(): Logger { + return this; + } + + /** + * Gets an array of log line objects. Each element is an object that corresponds to a log line. + */ + public getBufferedRecords(): LogLine[] { + if (!this.memoryLogger) { + throw new Error('getBufferedRecords is only supported when useMemoryLogging is true'); + } + return (this.memoryLogger?.loggedData as unknown as LogLine[]) ?? []; + } + + /** + * Reads a text blob of all the log lines contained in memory or the log file. + */ + public readLogContentsAsText(): string { + if (this.memoryLogger) { + return this.memoryLogger.loggedData.reduce((accum, line) => { + accum += JSON.stringify(line) + os.EOL; + return accum; + }, ''); + } else { + this.pinoLogger.warn( + 'readLogContentsAsText is not supported for file streams, only used when useMemoryLogging is true' + ); + const content = ''; + return content; + } + } + + /** + * @deprecated filters are no longer dynamic + * Adds a filter to be applied to all logged messages. + * + * @param filter A function with signature `(...args: any[]) => any[]` that transforms log message arguments. + */ + // eslint-disable-next-line @typescript-eslint/no-empty-function, class-methods-use-this, @typescript-eslint/no-unused-vars + public addFilter(filter: (...args: unknown[]) => unknown): void {} + + /** + * @deprecated. No longer necessary + * Close the logger, including any streams, and remove all listeners. + * + * @param fn A function with signature `(stream: LoggerStream) => void` to call for each stream with the stream as an arg. + */ + // eslint-disable-next-line @typescript-eslint/no-unused-vars, class-methods-use-this, @typescript-eslint/no-empty-function + public close(fn?: (stream: LoggerStream) => void): void {} + + /** + * 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 as `log:`. + * @param fields Additional fields included in all log lines for the child logger. + */ + public child(name: string, fields: Fields = {}): Logger { + 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. + */ + public addField(name: string, value: FieldValue): Logger { + 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 + public trace(...args: any[]): Logger { + this.pinoLogger.trace(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. + */ + public debug(...args: unknown[]): Logger { + this.pinoLogger.debug(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 + public debugCallback(cb: () => unknown[] | string): void {} + + /** + * Logs at `info` level with filtering applied. For convenience `this` object is returned. + * + * @param args Any number of arguments to be logged. + */ + public info(...args: unknown[]): Logger { + this.pinoLogger.info(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. + */ + public warn(...args: unknown[]): Logger { + this.pinoLogger.warn(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. + */ + public error(...args: unknown[]): Logger { + this.pinoLogger.error(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. + */ + public fatal(...args: unknown[]): Logger { + // 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(unwrapArray(args)); + return this; + } + + /** + * @deprecated use the env DEBUG=* or DEBUG= instead + * + * Enables logging to stdout when the DEBUG environment variable is used. It uses the logger + * name as the debug name, so you can do DEBUG= to filter the results to your logger. + */ + // eslint-disable-next-line class-methods-use-this, @typescript-eslint/no-empty-function + public enableDEBUG(): void {} /** 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('-')], + ['1d', new Date().toISOString().split('T')[0]], + ]); + const logRotationPeriod = new Env().getString('SF_LOG_ROTATION_PERIOD') ?? '1d'; + + return { + // write to a rotating file + target: 'pino/file', + options: { + destination: path.join(Global.SF_DIR, `sf.new-${rotator.get(logRotationPeriod) ?? '1d'}.log`), + mkdir: true, + level, + }, + }; +}; + +// TODO: how to clean up old logs using a new process +// TODO: telemetry as custom level (1) + +// TODO: should Lifecycle use logger instad of DEBUG + +// TODO: how to inject/hoist this into oclif to override DEBUG library and get telemetry from there? diff --git a/src/logger/memoryLogger.ts b/src/logger/memoryLogger.ts new file mode 100644 index 0000000000..67b47176e7 --- /dev/null +++ b/src/logger/memoryLogger.ts @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2023, 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 + */ +import { Writable } from 'stream'; +import { unwrapArray } from '../util/unwrapArray'; +import { filterSecrets } from './filters'; + +export class MemoryLogger extends Writable { + public loggedData: Array> = []; + + public constructor() { + super({ objectMode: true }); + } + + public _write(chunk: Record, encoding: string, callback: (err?: Error) => void): void { + const filteredChunk = unwrapArray(filterSecrets([chunk])); + this.loggedData.push( + typeof filteredChunk === 'string' + ? (JSON.parse(filteredChunk) as Record) + : (filteredChunk as Record) + ); + callback(); + } +} diff --git a/src/logger/transformStream.ts b/src/logger/transformStream.ts new file mode 100644 index 0000000000..c70f24991f --- /dev/null +++ b/src/logger/transformStream.ts @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2023, 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 + */ +import { pipeline, Transform } from 'stream'; +import { accessTokenRegex, sfdxAuthUrlRegex } from '../exported'; +import { unwrapArray } from '../util/unwrapArray'; +import { filterSecrets } from './filters'; + +// eslint-disable-next-line @typescript-eslint/no-var-requires, @typescript-eslint/no-unsafe-assignment +const build = require('pino-abstract-transport'); + +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type, @typescript-eslint/require-await, @typescript-eslint/no-unused-vars +export default async function (options: Record) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-call + return build( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (source: any): Transform => { + const myTransportStream = new Transform({ + objectMode: true, + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type + transform(chunk: Record, enc, cb) { + if (debugAllows(chunk)) { + // uses the original logger's filters. + const filteredChunk = unwrapArray(filterSecrets([chunk])); + + // stringify the payload again to make it easier to run replacements on + // filter things that look like certain tokens + const stringified = JSON.stringify(filteredChunk) + .replace(new RegExp(accessTokenRegex, 'g'), '') + .replace(new RegExp(sfdxAuthUrlRegex, 'g'), ''); + this.push(`${stringified}\n`); + } + cb(); + }, + }); + // eslint-disable-next-line @typescript-eslint/no-empty-function + pipeline(source, myTransportStream, () => {}); + return myTransportStream; + }, + { + // This is needed to be able to pipeline transports. + enablePipelining: true, + } + ); +} + +/** if the DEBUG= is set, see if that matches the logger name. If not, we don't want to keep going */ +const debugAllows = (chunk: Record): boolean => { + if (!process.env.DEBUG) return true; + if (process.env.DEBUG === '*') return true; + if (typeof chunk.name !== 'string') return true; + // turn wildcard patterns into regexes + const regexFromDebug = new RegExp(process.env.DEBUG.replace(/\*/g, '.*')); + if (!regexFromDebug.test(chunk.name)) { + // console.log(`no match : ${chunk.name} for ${process.env.DEBUG}`); + return false; + } else { + // console.log(`match : ${chunk.name} for ${process.env.DEBUG}`); + return true; + } +}; diff --git a/src/org/authInfo.ts b/src/org/authInfo.ts index 1cc6f0c590..97bc952ad1 100644 --- a/src/org/authInfo.ts +++ b/src/org/authInfo.ts @@ -30,7 +30,7 @@ import Transport from 'jsforce/lib/transport'; import * as jwt from 'jsonwebtoken'; import { Config } from '../config/config'; import { ConfigAggregator } from '../config/configAggregator'; -import { Logger } from '../logger'; +import { Logger } from '../logger/logger'; import { SfError } from '../sfError'; import { matchesAccessToken, trimTo15 } from '../util/sfdc'; import { StateAggregator } from '../stateAggregator'; diff --git a/src/org/authRemover.ts b/src/org/authRemover.ts index f0f6440237..fef9a8471e 100644 --- a/src/org/authRemover.ts +++ b/src/org/authRemover.ts @@ -8,7 +8,7 @@ import { AsyncOptionalCreatable } from '@salesforce/kit'; import { JsonMap } from '@salesforce/ts-types'; import { ConfigAggregator } from '../config/configAggregator'; -import { Logger } from '../logger'; +import { Logger } from '../logger/logger'; import { Messages } from '../messages'; import { StateAggregator } from '../stateAggregator'; import { OrgConfigProperties } from './orgConfigProperties'; diff --git a/src/org/connection.ts b/src/org/connection.ts index c3ed1121b7..ff526ccae8 100644 --- a/src/org/connection.ts +++ b/src/org/connection.ts @@ -25,7 +25,7 @@ import { Tooling as JSForceTooling } from 'jsforce/lib/api/tooling'; import { StreamPromise } from 'jsforce/lib/util/promise'; import { MyDomainResolver } from '../status/myDomainResolver'; import { ConfigAggregator } from '../config/configAggregator'; -import { Logger } from '../logger'; +import { Logger } from '../logger/logger'; import { SfError } from '../sfError'; import { validateApiVersion } from '../util/sfdc'; import { Messages } from '../messages'; diff --git a/src/org/org.ts b/src/org/org.ts index 713c780905..b21586d1ba 100644 --- a/src/org/org.ts +++ b/src/org/org.ts @@ -30,7 +30,7 @@ import { ConfigContents } from '../config/configStore'; import { OrgUsersConfig } from '../config/orgUsersConfig'; import { Global } from '../global'; import { Lifecycle } from '../lifecycleEvents'; -import { Logger } from '../logger'; +import { Logger } from '../logger/logger'; import { SfError } from '../sfError'; import { trimTo15 } from '../util/sfdc'; import { WebOAuthServer } from '../webOAuthServer'; diff --git a/src/org/permissionSetAssignment.ts b/src/org/permissionSetAssignment.ts index 1247a61cf9..883af9f2b3 100644 --- a/src/org/permissionSetAssignment.ts +++ b/src/org/permissionSetAssignment.ts @@ -9,7 +9,7 @@ import { EOL } from 'os'; import { mapKeys, upperFirst } from '@salesforce/kit'; import { hasArray, Optional } from '@salesforce/ts-types'; import { QueryResult, Record } from 'jsforce'; -import { Logger } from '../logger'; +import { Logger } from '../logger/logger'; import { Messages } from '../messages'; import { SfError } from '../sfError'; import { Org } from './org'; diff --git a/src/org/scratchOrgCreate.ts b/src/org/scratchOrgCreate.ts index f2bae0b714..38c143ac37 100644 --- a/src/org/scratchOrgCreate.ts +++ b/src/org/scratchOrgCreate.ts @@ -7,7 +7,7 @@ import { Duration } from '@salesforce/kit'; import { ensureString } from '@salesforce/ts-types'; import { Messages } from '../messages'; -import { Logger } from '../logger'; +import { Logger } from '../logger/logger'; import { ConfigAggregator } from '../config/configAggregator'; import { OrgConfigProperties } from '../org/orgConfigProperties'; import { SfProject } from '../sfProject'; diff --git a/src/org/scratchOrgErrorCodes.ts b/src/org/scratchOrgErrorCodes.ts index 9406ca3ae1..35ff3f7c5b 100644 --- a/src/org/scratchOrgErrorCodes.ts +++ b/src/org/scratchOrgErrorCodes.ts @@ -8,7 +8,7 @@ import { Optional } from '@salesforce/ts-types'; import { Messages } from '../messages'; import { SfError } from '../sfError'; -import { Logger } from '../logger'; +import { Logger } from '../logger/logger'; import { ScratchOrgInfo } from './scratchOrgTypes'; import { ScratchOrgCache } from './scratchOrgCache'; import { emit } from './scratchOrgLifecycleEvents'; diff --git a/src/org/scratchOrgInfoApi.ts b/src/org/scratchOrgInfoApi.ts index 9e42278659..c8fdfe893c 100644 --- a/src/org/scratchOrgInfoApi.ts +++ b/src/org/scratchOrgInfoApi.ts @@ -10,7 +10,7 @@ import { AnyJson } from '@salesforce/ts-types'; import { OAuth2Config, SaveResult } from 'jsforce'; import { retryDecorator, RetryError } from 'ts-retry-promise'; import { JwtOAuth2Config } from '../org/authInfo'; -import { Logger } from '../logger'; +import { Logger } from '../logger/logger'; import { Messages } from '../messages'; import { SfError } from '../sfError'; import { SfdcUrl } from '../util/sfdcUrl'; diff --git a/src/org/scratchOrgSettingsGenerator.ts b/src/org/scratchOrgSettingsGenerator.ts index fa164d5967..e5b9460af2 100644 --- a/src/org/scratchOrgSettingsGenerator.ts +++ b/src/org/scratchOrgSettingsGenerator.ts @@ -9,7 +9,7 @@ import * as path from 'path'; import { isEmpty, env, upperFirst, Duration } from '@salesforce/kit'; import { ensureObject, JsonMap } from '@salesforce/ts-types'; import * as js2xmlparser from 'js2xmlparser'; -import { Logger } from '../logger'; +import { Logger } from '../logger/logger'; import { SfError } from '../sfError'; import { StructuredWriter } from '../util/structuredWriter'; import { StatusResult } from '../status/types'; diff --git a/src/org/user.ts b/src/org/user.ts index d3d0ea8e11..83d6a71794 100644 --- a/src/org/user.ts +++ b/src/org/user.ts @@ -10,7 +10,7 @@ import { AsyncCreatable, lowerFirst, mapKeys, omit, parseJsonMap, upperFirst } f import { asJsonArray, asNumber, ensureJsonMap, ensureString, isJsonMap, Many } from '@salesforce/ts-types'; import type { HttpRequest, HttpResponse, QueryResult, Schema, SObjectUpdateRecord } from 'jsforce'; import { HttpApi } from 'jsforce/lib/http-api'; -import { Logger } from '../logger'; +import { Logger } from '../logger/logger'; import { Messages } from '../messages'; import { SecureBuffer } from '../crypto/secureBuffer'; import { SfError } from '../sfError'; diff --git a/src/schema/printer.ts b/src/schema/printer.ts index 7202ee4d23..8eab57b3d8 100644 --- a/src/schema/printer.ts +++ b/src/schema/printer.ts @@ -16,7 +16,7 @@ import { JsonMap, Optional, } from '@salesforce/ts-types'; -import { Logger } from '../logger'; +import { Logger } from '../logger/logger'; import { SfError } from '../sfError'; /** diff --git a/src/schema/validator.ts b/src/schema/validator.ts index 114332d2a5..2d16f9406c 100644 --- a/src/schema/validator.ts +++ b/src/schema/validator.ts @@ -10,7 +10,7 @@ import * as fs from 'fs'; import Ajv, { DefinedError } from 'ajv'; import { AnyJson, JsonMap } from '@salesforce/ts-types'; import { getJsonValuesByName, parseJsonMap } from '@salesforce/kit'; -import { Logger } from '../logger'; +import { Logger } from '../logger/logger'; import { SfError } from '../sfError'; /** diff --git a/src/stateAggregator/accessors/orgAccessor.ts b/src/stateAggregator/accessors/orgAccessor.ts index 263ef157e1..2fa317aaac 100644 --- a/src/stateAggregator/accessors/orgAccessor.ts +++ b/src/stateAggregator/accessors/orgAccessor.ts @@ -14,7 +14,7 @@ import { Global } from '../../global'; import { AuthFields } from '../../org'; import { ConfigFile } from '../../config/configFile'; import { ConfigContents } from '../../config/configStore'; -import { Logger } from '../../logger'; +import { Logger } from '../../logger/logger'; import { Messages } from '../../messages'; import { Lifecycle } from '../../lifecycleEvents'; diff --git a/src/status/myDomainResolver.ts b/src/status/myDomainResolver.ts index 8339138d2f..25dc42ceea 100644 --- a/src/status/myDomainResolver.ts +++ b/src/status/myDomainResolver.ts @@ -12,7 +12,7 @@ import { promisify } from 'util'; import { ensureString } from '@salesforce/ts-types'; import { AsyncOptionalCreatable, Duration, Env } from '@salesforce/kit'; -import { Logger } from '../logger'; +import { Logger } from '../logger/logger'; import { SfdcUrl } from '../util/sfdcUrl'; import { StatusResult } from './types'; import { PollingClient } from './pollingClient'; diff --git a/src/status/pollingClient.ts b/src/status/pollingClient.ts index f00d110bd4..97cd6667a0 100644 --- a/src/status/pollingClient.ts +++ b/src/status/pollingClient.ts @@ -7,7 +7,7 @@ import { AsyncOptionalCreatable, Duration } from '@salesforce/kit'; import { AnyJson, ensure } from '@salesforce/ts-types'; import { retryDecorator, NotRetryableError } from 'ts-retry-promise'; -import { Logger } from '../logger'; +import { Logger } from '../logger/logger'; import { SfError } from '../sfError'; import { Lifecycle } from '../lifecycleEvents'; import { StatusResult } from './types'; diff --git a/src/status/streamingClient.ts b/src/status/streamingClient.ts index ca5b2e7eb9..700a911555 100644 --- a/src/status/streamingClient.ts +++ b/src/status/streamingClient.ts @@ -11,7 +11,7 @@ import { resolve as resolveUrl } from 'url'; import { AsyncOptionalCreatable, Duration, Env, env, set } from '@salesforce/kit/lib'; import { AnyFunction, AnyJson, ensure, ensureString, JsonMap } from '@salesforce/ts-types/lib'; import * as Faye from 'faye'; -import { Logger } from '../logger'; +import { Logger } from '../logger/logger'; import { Org } from '../org/org'; import { SfError } from '../sfError'; import { Messages } from '../messages'; diff --git a/src/testSetup.ts b/src/testSetup.ts index 00776b79ea..e4ac92fe8e 100644 --- a/src/testSetup.ts +++ b/src/testSetup.ts @@ -35,7 +35,7 @@ import { ConfigFile } from './config/configFile'; import { ConfigContents } from './config/configStore'; import { Connection } from './org/connection'; import { Crypto } from './crypto/crypto'; -import { Logger } from './logger'; +import { Logger } from './logger/logger'; import { Messages } from './messages'; import { SfError } from './sfError'; import { SfProject, SfProjectJson } from './sfProject'; @@ -149,7 +149,7 @@ export class TestContext { ORGS: sinon.createSandbox(), }; - this.TEST_LOGGER = new Logger({ name: 'SFDX_Core_Test_Logger' }).useMemoryLogging(); + this.TEST_LOGGER = new Logger({ name: 'SFDX_Core_Test_Logger', useMemoryLogger: true }); if (opts.setup) { this.setup(); @@ -691,6 +691,8 @@ export const restoreContext = (testContext: TestContext): void => { testContext.configStubs = {}; // Give each test run a clean StateAggregator StateAggregator.clearInstance(); + // get a new clean logger for each run + testContext.TEST_LOGGER.close(); // Allow each test to have their own config aggregator // @ts-ignore clear for testing. delete ConfigAggregator.instance; diff --git a/src/util/sfdc.ts b/src/util/sfdc.ts index 3942f2999e..1c0d756d81 100644 --- a/src/util/sfdc.ts +++ b/src/util/sfdc.ts @@ -86,9 +86,13 @@ export const findUpperCaseKeys = (data?: JsonMap, sectionBlocklist: string[] = [ return key; }; +export const accessTokenRegex = /(00D\w{12,15})![.\w]*/; +export const sfdxAuthUrlRegex = + /force:\/\/([a-zA-Z0-9._-]+):([a-zA-Z0-9._-]*):([a-zA-Z0-9._-]+={0,2})@([a-zA-Z0-9._-]+)/; + /** * Tests whether a given string is an access token * * @param value */ -export const matchesAccessToken = (value: string): boolean => /^(00D\w{12,15})![.\w]*$/.test(value); +export const matchesAccessToken = (value: string): boolean => accessTokenRegex.test(value); diff --git a/src/util/sfdcUrl.ts b/src/util/sfdcUrl.ts index 853c307208..73f1c7829a 100644 --- a/src/util/sfdcUrl.ts +++ b/src/util/sfdcUrl.ts @@ -9,7 +9,7 @@ import { URL } from 'url'; import { Env, Duration } from '@salesforce/kit'; import { ensureNumber, ensureArray } from '@salesforce/ts-types'; import { MyDomainResolver } from '../status/myDomainResolver'; -import { Logger } from '../logger'; +import { Logger } from '../logger/logger'; import { Lifecycle } from '../lifecycleEvents'; export function getLoginAudienceCombos(audienceUrl: string, loginUrl: string): Array<[string, string]> { diff --git a/src/util/unwrapArray.ts b/src/util/unwrapArray.ts new file mode 100644 index 0000000000..cb7be7f4c5 --- /dev/null +++ b/src/util/unwrapArray.ts @@ -0,0 +1,12 @@ +/* + * Copyright (c) 2023, 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 + */ +export const unwrapArray = (args: unknown): unknown => { + if (Array.isArray(args) && args.length === 1) { + return Array.isArray(args[0]) ? unwrapArray(args[0]) : args[0]; + } + return args; +}; diff --git a/src/util/zipWriter.ts b/src/util/zipWriter.ts index a8778abab5..a53bf89f1b 100644 --- a/src/util/zipWriter.ts +++ b/src/util/zipWriter.ts @@ -7,7 +7,7 @@ import { Readable, Writable } from 'stream'; import * as JSZip from 'jszip'; -import { Logger } from '../logger'; +import { Logger } from '../logger/logger'; import { SfError } from '../sfError'; import { StructuredWriter } from './structuredWriter'; diff --git a/src/webOAuthServer.ts b/src/webOAuthServer.ts index b7633eb28c..9ba93cea60 100644 --- a/src/webOAuthServer.ts +++ b/src/webOAuthServer.ts @@ -15,7 +15,7 @@ import { EventEmitter } from 'events'; import { OAuth2 } from 'jsforce'; import { AsyncCreatable, Env, set, toNumber } from '@salesforce/kit'; import { asString, get, Nullable } from '@salesforce/ts-types'; -import { Logger } from './logger'; +import { Logger } from './logger/logger'; import { AuthInfo, DEFAULT_CONNECTED_APP_INFO } from './org'; import { SfError } from './sfError'; import { Messages } from './messages'; diff --git a/test/unit/logger/filterTest.ts b/test/unit/logger/filterTest.ts new file mode 100644 index 0000000000..25f06a88c1 --- /dev/null +++ b/test/unit/logger/filterTest.ts @@ -0,0 +1,111 @@ +/* + * Copyright (c) 2023, 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 + */ +import { assert, expect } from 'chai'; +import { filterSecrets, HIDDEN } from '../../../src/logger/filters'; +import { unwrapArray } from '../../../src/util/unwrapArray'; + +describe('filters', () => { + const sid = '00D55000000M2qA!AQ0AQHg3LnYDOyobmH07'; + const simpleString = `sid=${sid}`; + const stringWithObject = ` The rain in Spain: ${JSON.stringify({ + // eslint-disable-next-line camelcase + access_token: sid, + })}`; + const obj1 = { accessToken: `${sid}`, refreshToken: `${sid}` }; + const obj2 = { key: 'Access Token', value: `${sid}` }; + const arr1 = [ + { key: 'ACCESS token ', value: `${sid}` }, + { key: 'refresh TOKEN', value: `${sid}` }, + { key: 'Sfdx Auth Url', value: `${sid}` }, + ]; + const arr2 = [ + { key: ' AcCESS 78token', value: ` ${sid} ` }, + { key: ' refresh _TOKEn ', value: ` ${sid} ` }, + { key: ' SfdX__AuthUrl ', value: ` ${sid} ` }, + ]; + + it(`filters ${simpleString} correctly`, () => { + const result = getUnwrapped(simpleString); + expect(result).to.not.contain(sid); + expect(result).to.contain(HIDDEN); + }); + + it(`filters ${stringWithObject} correctly`, () => { + const result = getUnwrapped(stringWithObject); + expect(result).to.not.contain(sid); + expect(result).to.contain(HIDDEN); + }); + + it('filters obj1 correctly', () => { + const result = getUnwrapped(obj1); + assert(result); + Object.entries(result).forEach(([, value]) => { + expect(value).to.not.contain(sid); + expect(value).to.contain(HIDDEN); + }); + }); + + it('filters obj2 correctly', () => { + const result = getUnwrapped(obj2); + assert(result); + Object.entries(result).forEach(([, value]) => { + expect(value).to.not.contain(sid); + expect(value).to.contain(HIDDEN); + }); + }); + + it('filters arr1 correctly', () => { + const result = getUnwrapped(arr1); + assert(result); + assert(Array.isArray(result)); + result.forEach((item: Record) => { + assert(item); + Object.entries(item).forEach(([, value]) => { + expect(value).to.not.contain(sid); + expect(value).to.contain(HIDDEN); + }); + }); + }); + + it('filters arr2 correctly', () => { + const result = getUnwrapped(arr2); + assert(result); + assert(Array.isArray(result)); + result.forEach((item: Record) => { + assert(item); + Object.entries(item).forEach(([, value]) => { + expect(value).to.not.contain(sid); + expect(value).to.contain(HIDDEN); + }); + }); + }); + + describe('does not filter innocent stuff', () => { + it('basic string', () => { + const result = getUnwrapped('some string'); + expect(result).to.equal('some string'); + }); + it('basic object', () => { + const input = { foo: 'bar' }; + const result = getUnwrapped(input); + expect(result).to.deep.equal(input); + }); + it('basic array', () => { + const input = ['foo', 'bar']; + const result = getUnwrapped(input); + expect(result).to.deep.equal(input); + }); + it('object with one bad prop', () => { + const input = { foo: 'bar', accessToken: `${sid}` }; + const result = getUnwrapped(input); + expect(result).to.have.property('foo', 'bar'); + expect(result).to.have.property('accessToken').contains(HIDDEN); + }); + }); +}); + +const getUnwrapped = (input: unknown): unknown => unwrapArray(filterSecrets(input)); diff --git a/test/unit/loggerTest.ts b/test/unit/loggerTest.ts index 9b12a89722..57168dcfc3 100644 --- a/test/unit/loggerTest.ts +++ b/test/unit/loggerTest.ts @@ -8,17 +8,14 @@ /* eslint-disable @typescript-eslint/no-unsafe-call */ /* eslint-disable @typescript-eslint/no-unsafe-return */ -import * as fs from 'fs'; -import { isBoolean, isNumber, isString } from '@salesforce/ts-types'; -import { expect } from 'chai'; -import * as debug from 'debug'; -import * as _ from 'lodash'; -import { Logger, LoggerFormat, LoggerLevel, LoggerStream } from '../../src/logger'; +import { expect, config as chaiConfig } from 'chai'; +import { Logger, LoggerLevel } from '../../src/logger/logger'; import { shouldThrowSync, TestContext } from '../../src/testSetup'; // NOTE: These tests still use 'await' which is how it use to work and were left to make // sure we didn't regress the way they were used. +chaiConfig.truncateThreshold = 0; describe('Logger', () => { const $$ = new TestContext(); const sfdxEnv = process.env.SFDX_ENV; @@ -42,7 +39,7 @@ describe('Logger', () => { describe('constructor', () => { it('should construct a new named logger', async () => { - const logger1 = new Logger('testLogger'); + const logger1 = new Logger({ name: 'testLogger', useMemoryLogger: true }); expect(logger1).to.be.instanceof(Logger); expect(logger1.getName()).to.equal('testLogger'); const logger2 = await Logger.root(); @@ -52,7 +49,7 @@ describe('Logger', () => { describe('levels', () => { it('should set the log level using a number', () => { - const logger = new Logger('testLogger'); + const logger = new Logger({ name: 'testLogger', useMemoryLogger: true }); logger.setLevel(LoggerLevel.ERROR); expect(logger.getLevel()).to.equal(LoggerLevel.ERROR); logger.setLevel(); @@ -60,7 +57,7 @@ describe('Logger', () => { }); it('should set the log level using a string', () => { - const logger = new Logger('testLogger'); + const logger = new Logger({ name: 'testLogger', useMemoryLogger: true }); logger.setLevel(Logger.getLevelByName('ERROR')); expect(logger.getLevel()).to.equal(LoggerLevel.ERROR); logger.setLevel(Logger.getLevelByName('warn')); @@ -85,7 +82,7 @@ describe('Logger', () => { describe('shouldLog', () => { it('returns correct boolean', () => { - const logger = new Logger('test'); + const logger = new Logger({ name: 'test', useMemoryLogger: true }); logger.setLevel(); expect(logger.shouldLog(LoggerLevel.ERROR)).to.be.true; expect(logger.shouldLog(LoggerLevel.WARN)).to.be.true; @@ -94,58 +91,6 @@ describe('Logger', () => { expect(logger.shouldLog(LoggerLevel.INFO)).to.be.true; expect(logger.shouldLog(LoggerLevel.DEBUG)).to.be.true; expect(logger.shouldLog(LoggerLevel.TRACE)).to.be.false; - logger.setLevel(7); - expect(logger.shouldLog(LoggerLevel.TRACE)).to.be.true; - }); - }); - - describe('addLogFileStream', () => { - const testLogFile = 'some/dir/mylogfile.json'; - let utilAccessStub: sinon.SinonStub; - let utilWriteFileStub: sinon.SinonStub; - - beforeEach(() => { - utilAccessStub = $$.SANDBOX.stub(fs.promises, 'access'); - utilWriteFileStub = $$.SANDBOX.stub(fs.promises, 'writeFile'); - }); - - it('should not create a new log file if it exists already', async () => { - utilAccessStub.resolves({}); - const logger = new Logger('test'); - const addStreamStub = $$.SANDBOX.stub(logger, 'addStream'); - await logger.addLogFileStream(testLogFile); - expect(utilAccessStub.firstCall.args[0]).to.equal(testLogFile); - expect(utilWriteFileStub.called).to.be.false; - const addStreamArgs = addStreamStub.firstCall.args[0]; - expect(addStreamArgs).to.have.property('type', 'rotating-file'); - expect(addStreamArgs).to.have.property('path', testLogFile); - expect(addStreamArgs).to.have.property('level', logger.getLevel()); - }); - - it('should allow log rotation count and period overrides', async () => { - process.env.SF_LOG_ROTATION_PERIOD = '1w'; - process.env.SF_LOG_ROTATION_COUNT = '3'; - - utilAccessStub.returns(Promise.resolve({})); - const logger = new Logger('testing-env-vars'); - const addStreamStub = $$.SANDBOX.stub(logger, 'addStream'); - await logger.addLogFileStream(testLogFile); - - const addStreamArgs = addStreamStub.firstCall.args[0]; - expect(addStreamArgs).to.have.property('period', '1w'); - expect(addStreamArgs).to.have.property('count', 3); - }); - - it('should create a new log file and all directories if nonexistent', async () => { - utilAccessStub.throws(); - const logger = new Logger('testLogger'); - const addStreamStub = $$.SANDBOX.stub(logger, 'addStream'); - await logger.addLogFileStream(testLogFile); - expect(utilAccessStub.firstCall.args[0]).to.equal(testLogFile); - expect(utilWriteFileStub.firstCall.args[0]).to.equal(testLogFile); - expect(utilWriteFileStub.firstCall.args[1]).to.equal(''); - expect(utilWriteFileStub.firstCall.args[2]).to.have.property('mode', '600'); - expect(addStreamStub.called).to.be.true; }); }); @@ -155,8 +100,6 @@ describe('Logger', () => { const defaultLogger = await Logger.root(); expect(defaultLogger).to.be.instanceof(Logger); expect(defaultLogger.getName()).to.equal('sf'); - // @ts-expect-error: called is a sinon spy property - expect(defaultLogger.addFilter['called'], 'new Logger() should have called addFilter()').to.be.true; const logger = await Logger.root(); expect(logger).to.equal(defaultLogger); }); @@ -178,18 +121,6 @@ describe('Logger', () => { // @ts-expect-error: called is a sinon spy property expect(rootLogger.addLogFileStream['called']).to.be.false; }); - - it('should log uncaught exception in root logger', async () => { - process.env.SFDX_ENV = 'dev'; - - const rootLogger = await Logger.root(); - $$.SANDBOX.stub(rootLogger, 'fatal'); - - // @ts-expect-error to access private property `lifecycle` for testing uncaughtException - Logger.lifecycle.emit('uncaughtException', 'testException'); - // @ts-expect-error: called is a sinon spy property - expect(rootLogger.fatal['called']).to.be.true; - }); }); describe('child', () => { @@ -199,93 +130,11 @@ describe('Logger', () => { expect(childLogger).to.be.instanceof(Logger); expect(childLogger.getName()).to.equal(childLoggerName); }); - - it('should not log uncaught exception in child logger', async () => { - process.env.SFDX_ENV = 'dev'; - - const childLoggerName = 'myChildLogger'; - const childLogger = await Logger.child(childLoggerName); - $$.SANDBOX.stub(childLogger, 'fatal'); - - // @ts-expect-error to access private property `lifecycle` for testing uncaughtException - Logger.lifecycle.emit('uncaughtException', 'testException'); - // @ts-expect-error: called is a sinon spy property - expect(childLogger.fatal['called']).to.be.false; - }); - }); - - describe('debugCallback', () => { - it('should log', async () => { - const logger = (await Logger.child('testLogger')).useMemoryLogging(); - logger.setLevel(LoggerLevel.DEBUG); - const FOO = 'foo'; - const BAR = 'bar'; - const spy = $$.SANDBOX.spy(() => [FOO, BAR]); - logger.debugCallback(spy); - expect(spy.callCount).to.be.equal(1); - expect(logger.readLogContentsAsText()).to.include(FOO).and.to.include(BAR); - }); - - it("shouldn't log", async () => { - const logger = (await Logger.child('testLogger')).useMemoryLogging(); - const fooSpy = $$.SANDBOX.spy(() => 'FOO'); - const cbSpy = $$.SANDBOX.spy(() => `${fooSpy()}`); - logger.debugCallback(cbSpy); - expect(fooSpy.callCount).to.be.equal(0); - expect(cbSpy.callCount).to.be.equal(0); - }); - }); - - describe('filters', () => { - const sid = '00D55000000M2qA!AQ0AQHg3LnYDOyobmH07'; - const simpleString = `sid=${sid}`; - const stringWithObject = ` The rain in Spain: ${JSON.stringify({ - // eslint-disable-next-line camelcase - access_token: sid, - })}`; - const obj1 = { accessToken: `${sid}`, refreshToken: `${sid}` }; - const obj2 = { key: 'Access Token', value: `${sid}` }; - const arr1 = [ - { key: 'ACCESS token ', value: `${sid}` }, - { key: 'refresh TOKEN', value: `${sid}` }, - { key: 'Sfdx Auth Url', value: `${sid}` }, - ]; - const arr2 = [ - { key: ' AcCESS 78token', value: ` ${sid} ` }, - { key: ' refresh _TOKEn ', value: ` ${sid} ` }, - { key: ' SfdX__AuthUrl ', value: ` ${sid} ` }, - ]; - const testLogEntries = [simpleString, stringWithObject, obj1, obj2, arr1, arr2]; - - async function runTest(logLevel: [string, number]) { - const logger = (await Logger.child('testLogger')).useMemoryLogging().setLevel(0); - - // Log at the provided log level for each test entry - // @ts-expect-error suppress any type - testLogEntries.forEach((entry) => logger[logLevel[0]](entry)); - - const logData = logger.readLogContentsAsText(); - expect(logData, `Logs should NOT contain '${sid}'`).to.not.contain(sid); - const logRecords = logger.getBufferedRecords(); - expect(logRecords[0], `expected to log at level: ${logLevel[0]}`).to.have.property('level', logLevel[1]); - } - - it('should apply for log level: trace', () => runTest(['trace', 10])); - - it('should apply for log level: debug', () => runTest(['debug', 20])); - - it('should apply for log level: info', () => runTest(['info', 30])); - - it('should apply for log level: warn', () => runTest(['warn', 40])); - - it('should apply for log level: error', () => runTest(['error', 50])); - - it('should apply for log level: fatal', () => runTest(['fatal', 60])); }); describe('addField', () => { it('should add a field to the log record', async () => { - const logger = (await Logger.child('testLogger')).useMemoryLogging(); + const logger = await Logger.child('testLogger'); logger.addField('newField1', 'stringVal'); logger.addField('newField2', 9); logger.addField('newField3', true); @@ -299,145 +148,4 @@ describe('Logger', () => { expect(logRecords[0]).to.have.property('newField3', true); }); }); - - describe('serializers', () => { - it('should run properly after filters are applied', async () => { - const logger = (await Logger.child('testSerializersLogger')).useMemoryLogging(); - - // A test serializer - logger.getBunyanLogger().serializers.config = (obj: Record) => - _.reduce( - obj, - (acc, val, key) => { - if (isString(val) || isNumber(val) || isBoolean(val)) { - // @ts-expect-error string cannot index value - acc[key] = val; - } - return acc; - }, - {} - ); - - logger.warn({ config: { foo: { bar: 1 }, sid: 'secret' } }); - const logRecords = logger.getBufferedRecords(); - - // If the serializer was applied it should not log the 'foo' entry - const msgOnError = 'Expected the config serializer to remove the "foo" entry from the log record '; - expect(logRecords[0], msgOnError).to.have.deep.property('config', { - sid: '