1 | ;
|
2 | Object.defineProperty(exports, "__esModule", { value: true });
|
3 | exports.Logger = exports.LoggerFormat = exports.LoggerLevel = void 0;
|
4 | /*
|
5 | * Copyright (c) 2020, salesforce.com, inc.
|
6 | * All rights reserved.
|
7 | * Licensed under the BSD 3-Clause license.
|
8 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
|
9 | */
|
10 | const events_1 = require("events");
|
11 | const os = require("os");
|
12 | const path = require("path");
|
13 | const stream_1 = require("stream");
|
14 | const fs = require("fs");
|
15 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
16 | // @ts-ignore
|
17 | const Bunyan = require("@salesforce/bunyan");
|
18 | const kit_1 = require("@salesforce/kit");
|
19 | const ts_types_1 = require("@salesforce/ts-types");
|
20 | const Debug = require("debug");
|
21 | const global_1 = require("./global");
|
22 | const sfError_1 = require("./sfError");
|
23 | /**
|
24 | * Standard `Logger` levels.
|
25 | *
|
26 | * **See** {@link https://github.com/forcedotcom/node-bunyan#levels|Bunyan Levels}
|
27 | */
|
28 | var LoggerLevel;
|
29 | (function (LoggerLevel) {
|
30 | LoggerLevel[LoggerLevel["TRACE"] = 10] = "TRACE";
|
31 | LoggerLevel[LoggerLevel["DEBUG"] = 20] = "DEBUG";
|
32 | LoggerLevel[LoggerLevel["INFO"] = 30] = "INFO";
|
33 | LoggerLevel[LoggerLevel["WARN"] = 40] = "WARN";
|
34 | LoggerLevel[LoggerLevel["ERROR"] = 50] = "ERROR";
|
35 | LoggerLevel[LoggerLevel["FATAL"] = 60] = "FATAL";
|
36 | })(LoggerLevel = exports.LoggerLevel || (exports.LoggerLevel = {}));
|
37 | /**
|
38 | * `Logger` format types.
|
39 | */
|
40 | var LoggerFormat;
|
41 | (function (LoggerFormat) {
|
42 | LoggerFormat[LoggerFormat["JSON"] = 0] = "JSON";
|
43 | LoggerFormat[LoggerFormat["LOGFMT"] = 1] = "LOGFMT";
|
44 | })(LoggerFormat = exports.LoggerFormat || (exports.LoggerFormat = {}));
|
45 | /**
|
46 | * A logging abstraction powered by {@link https://github.com/forcedotcom/node-bunyan|Bunyan} that provides both a default
|
47 | * logger configuration that will log to `sfdx.log`, and a way to create custom loggers based on the same foundation.
|
48 | *
|
49 | * ```
|
50 | * // Gets the root sfdx logger
|
51 | * const logger = await Logger.root();
|
52 | *
|
53 | * // Creates a child logger of the root sfdx logger with custom fields applied
|
54 | * const childLogger = await Logger.child('myRootChild', {tag: 'value'});
|
55 | *
|
56 | * // Creates a custom logger unaffiliated with the root logger
|
57 | * const myCustomLogger = new Logger('myCustomLogger');
|
58 | *
|
59 | * // Creates a child of a custom logger unaffiliated with the root logger with custom fields applied
|
60 | * const myCustomChildLogger = myCustomLogger.child('myCustomChild', {tag: 'value'});
|
61 | * ```
|
62 | * **See** https://github.com/forcedotcom/node-bunyan
|
63 | *
|
64 | * **See** https://developer.salesforce.com/docs/atlas.en-us.sfdx_setup.meta/sfdx_setup/sfdx_dev_cli_log_messages.htm
|
65 | */
|
66 | class Logger {
|
67 | /**
|
68 | * Constructs a new `Logger`.
|
69 | *
|
70 | * @param optionsOrName A set of `LoggerOptions` or name to use with the default options.
|
71 | *
|
72 | * **Throws** *{@link SfError}{ name: 'RedundantRootLoggerError' }* More than one attempt is made to construct the root
|
73 | * `Logger`.
|
74 | */
|
75 | constructor(optionsOrName) {
|
76 | /**
|
77 | * The default rotation period for logs. Example '1d' will rotate logs daily (at midnight).
|
78 | * See 'period' docs here: https://github.com/forcedotcom/node-bunyan#stream-type-rotating-file
|
79 | */
|
80 | this.logRotationPeriod = new kit_1.Env().getString('SF_LOG_ROTATION_PERIOD') || '1d';
|
81 | /**
|
82 | * The number of backup rotated log files to keep.
|
83 | * Example: '3' will have the base sf.log file, and the past 3 (period) log files.
|
84 | * See 'count' docs here: https://github.com/forcedotcom/node-bunyan#stream-type-rotating-file
|
85 | */
|
86 | this.logRotationCount = new kit_1.Env().getNumber('SF_LOG_ROTATION_COUNT') || 2;
|
87 | /**
|
88 | * Whether debug is enabled for this Logger.
|
89 | */
|
90 | this.debugEnabled = false;
|
91 | this.uncaughtExceptionHandler = (err) => {
|
92 | // W-7558552
|
93 | // Only log uncaught exceptions in root logger
|
94 | if (this === Logger.rootLogger) {
|
95 | // log the exception
|
96 | // FIXME: good chance this won't be logged because
|
97 | // process.exit was called before this is logged
|
98 | // https://github.com/trentm/node-bunyan/issues/95
|
99 | this.fatal(err);
|
100 | }
|
101 | };
|
102 | this.exitHandler = () => {
|
103 | this.close();
|
104 | };
|
105 | let options;
|
106 | if (typeof optionsOrName === 'string') {
|
107 | options = {
|
108 | name: optionsOrName,
|
109 | level: Logger.DEFAULT_LEVEL,
|
110 | serializers: Bunyan.stdSerializers,
|
111 | };
|
112 | }
|
113 | else {
|
114 | options = optionsOrName;
|
115 | }
|
116 | if (Logger.rootLogger && options.name === Logger.ROOT_NAME) {
|
117 | throw new sfError_1.SfError('Can not create another root logger.', 'RedundantRootLoggerError');
|
118 | }
|
119 | // Inspect format to know what logging format to use then delete from options to
|
120 | // ensure it doesn't conflict with Bunyan.
|
121 | this.format = options.format || LoggerFormat.JSON;
|
122 | delete options.format;
|
123 | // If the log format is LOGFMT, we need to convert any stream(s) into a LOGFMT type stream.
|
124 | if (this.format === LoggerFormat.LOGFMT && options.stream) {
|
125 | const ls = this.createLogFmtFormatterStream({ stream: options.stream });
|
126 | options.stream = ls.stream;
|
127 | }
|
128 | if (this.format === LoggerFormat.LOGFMT && options.streams) {
|
129 | const logFmtConvertedStreams = [];
|
130 | options.streams.forEach((ls) => {
|
131 | logFmtConvertedStreams.push(this.createLogFmtFormatterStream(ls));
|
132 | });
|
133 | options.streams = logFmtConvertedStreams;
|
134 | }
|
135 | this.bunyan = new Bunyan(options);
|
136 | this.bunyan.name = options.name;
|
137 | this.bunyan.filters = [];
|
138 | if (!options.streams && !options.stream) {
|
139 | this.bunyan.streams = [];
|
140 | }
|
141 | // all SFDX loggers must filter sensitive data
|
142 | this.addFilter((...args) => _filter(...args));
|
143 | if (global_1.Global.getEnvironmentMode() !== global_1.Mode.TEST) {
|
144 | Logger.lifecycle.on('uncaughtException', this.uncaughtExceptionHandler);
|
145 | Logger.lifecycle.on('exit', this.exitHandler);
|
146 | }
|
147 | this.trace(`Created '${this.getName()}' logger instance`);
|
148 | }
|
149 | /**
|
150 | * Gets the root logger with the default level, file stream, and DEBUG enabled.
|
151 | */
|
152 | static async root() {
|
153 | if (this.rootLogger) {
|
154 | return this.rootLogger;
|
155 | }
|
156 | const rootLogger = (this.rootLogger = new Logger(Logger.ROOT_NAME).setLevel());
|
157 | // disable log file writing, if applicable
|
158 | if (process.env.SFDX_DISABLE_LOG_FILE !== 'true' && global_1.Global.getEnvironmentMode() !== global_1.Mode.TEST) {
|
159 | await rootLogger.addLogFileStream(global_1.Global.LOG_FILE_PATH);
|
160 | }
|
161 | rootLogger.enableDEBUG();
|
162 | return rootLogger;
|
163 | }
|
164 | /**
|
165 | * Gets the root logger with the default level, file stream, and DEBUG enabled.
|
166 | */
|
167 | static getRoot() {
|
168 | if (this.rootLogger) {
|
169 | return this.rootLogger;
|
170 | }
|
171 | const rootLogger = (this.rootLogger = new Logger(Logger.ROOT_NAME).setLevel());
|
172 | // disable log file writing, if applicable
|
173 | if (process.env.SFDX_DISABLE_LOG_FILE !== 'true' && global_1.Global.getEnvironmentMode() !== global_1.Mode.TEST) {
|
174 | rootLogger.addLogFileStreamSync(global_1.Global.LOG_FILE_PATH);
|
175 | }
|
176 | rootLogger.enableDEBUG();
|
177 | return rootLogger;
|
178 | }
|
179 | /**
|
180 | * Destroys the root `Logger`.
|
181 | *
|
182 | * @ignore
|
183 | */
|
184 | static destroyRoot() {
|
185 | if (this.rootLogger) {
|
186 | this.rootLogger.close();
|
187 | this.rootLogger = undefined;
|
188 | }
|
189 | }
|
190 | /**
|
191 | * Create a child of the root logger, inheriting this instance's configuration such as `level`, `streams`, etc.
|
192 | *
|
193 | * @param name The name of the child logger.
|
194 | * @param fields Additional fields included in all log lines.
|
195 | */
|
196 | static async child(name, fields) {
|
197 | return (await Logger.root()).child(name, fields);
|
198 | }
|
199 | /**
|
200 | * Create a child of the root logger, inheriting this instance's configuration such as `level`, `streams`, etc.
|
201 | *
|
202 | * @param name The name of the child logger.
|
203 | * @param fields Additional fields included in all log lines.
|
204 | */
|
205 | static childFromRoot(name, fields) {
|
206 | return Logger.getRoot().child(name, fields);
|
207 | }
|
208 | /**
|
209 | * Gets a numeric `LoggerLevel` value by string name.
|
210 | *
|
211 | * @param {string} levelName The level name to convert to a `LoggerLevel` enum value.
|
212 | *
|
213 | * **Throws** *{@link SfError}{ name: 'UnrecognizedLoggerLevelNameError' }* The level name was not case-insensitively recognized as a valid `LoggerLevel` value.
|
214 | * @see {@Link LoggerLevel}
|
215 | */
|
216 | static getLevelByName(levelName) {
|
217 | levelName = levelName.toUpperCase();
|
218 | if (!(0, ts_types_1.isKeyOf)(LoggerLevel, levelName)) {
|
219 | throw new sfError_1.SfError(`Invalid log level "${levelName}".`, 'UnrecognizedLoggerLevelNameError');
|
220 | }
|
221 | return LoggerLevel[levelName];
|
222 | }
|
223 | /**
|
224 | * Adds a stream.
|
225 | *
|
226 | * @param stream The stream configuration to add.
|
227 | * @param defaultLevel The default level of the stream.
|
228 | */
|
229 | addStream(stream, defaultLevel) {
|
230 | if (this.format === LoggerFormat.LOGFMT) {
|
231 | stream = this.createLogFmtFormatterStream(stream);
|
232 | }
|
233 | this.bunyan.addStream(stream, defaultLevel);
|
234 | }
|
235 | /**
|
236 | * Adds a file stream to this logger. Resolved or rejected upon completion of the addition.
|
237 | *
|
238 | * @param logFile The path to the log file. If it doesn't exist it will be created.
|
239 | */
|
240 | async addLogFileStream(logFile) {
|
241 | try {
|
242 | // Check if we have write access to the log file (i.e., we created it already)
|
243 | await fs.promises.access(logFile, fs.constants.W_OK);
|
244 | }
|
245 | catch (err1) {
|
246 | try {
|
247 | if (process.platform === 'win32') {
|
248 | await fs.promises.mkdir(path.dirname(logFile), { recursive: true });
|
249 | }
|
250 | else {
|
251 | await fs.promises.mkdir(path.dirname(logFile), { recursive: true, mode: 0o700 });
|
252 | }
|
253 | }
|
254 | catch (err2) {
|
255 | throw sfError_1.SfError.wrap(err2);
|
256 | }
|
257 | try {
|
258 | await fs.promises.writeFile(logFile, '', { mode: '600' });
|
259 | }
|
260 | catch (err3) {
|
261 | throw sfError_1.SfError.wrap(err3);
|
262 | }
|
263 | }
|
264 | // avoid multiple streams to same log file
|
265 | if (!this.bunyan.streams.find(
|
266 | // No bunyan typings
|
267 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
|
268 | (stream) => stream.type === 'rotating-file' && stream.path === logFile)) {
|
269 | this.addStream({
|
270 | type: 'rotating-file',
|
271 | path: logFile,
|
272 | period: this.logRotationPeriod,
|
273 | count: this.logRotationCount,
|
274 | level: this.bunyan.level(),
|
275 | });
|
276 | }
|
277 | }
|
278 | /**
|
279 | * Adds a file stream to this logger. Resolved or rejected upon completion of the addition.
|
280 | *
|
281 | * @param logFile The path to the log file. If it doesn't exist it will be created.
|
282 | */
|
283 | addLogFileStreamSync(logFile) {
|
284 | try {
|
285 | // Check if we have write access to the log file (i.e., we created it already)
|
286 | fs.accessSync(logFile, fs.constants.W_OK);
|
287 | }
|
288 | catch (err1) {
|
289 | try {
|
290 | if (process.platform === 'win32') {
|
291 | fs.mkdirSync(path.dirname(logFile));
|
292 | }
|
293 | else {
|
294 | fs.mkdirSync(path.dirname(logFile), { mode: 0o700 });
|
295 | }
|
296 | }
|
297 | catch (err2) {
|
298 | throw sfError_1.SfError.wrap(err2);
|
299 | }
|
300 | try {
|
301 | fs.writeFileSync(logFile, '', { mode: '600' });
|
302 | }
|
303 | catch (err3) {
|
304 | throw sfError_1.SfError.wrap(err3);
|
305 | }
|
306 | }
|
307 | // avoid multiple streams to same log file
|
308 | if (!this.bunyan.streams.find(
|
309 | // No bunyan typings
|
310 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
|
311 | (stream) => stream.type === 'rotating-file' && stream.path === logFile)) {
|
312 | this.addStream({
|
313 | type: 'rotating-file',
|
314 | path: logFile,
|
315 | period: this.logRotationPeriod,
|
316 | count: this.logRotationCount,
|
317 | level: this.bunyan.level(),
|
318 | });
|
319 | }
|
320 | }
|
321 | /**
|
322 | * Gets the name of this logger.
|
323 | */
|
324 | getName() {
|
325 | return this.bunyan.name;
|
326 | }
|
327 | /**
|
328 | * Gets the current level of this logger.
|
329 | */
|
330 | getLevel() {
|
331 | return this.bunyan.level();
|
332 | }
|
333 | /**
|
334 | * Set the logging level of all streams for this logger. If a specific `level` is not provided, this method will
|
335 | * attempt to read it from the environment variable `SFDX_LOG_LEVEL`, and if not found,
|
336 | * {@link Logger.DEFAULT_LOG_LEVEL} will be used instead. For convenience `this` object is returned.
|
337 | *
|
338 | * @param {LoggerLevelValue} [level] The logger level.
|
339 | *
|
340 | * **Throws** *{@link SfError}{ name: 'UnrecognizedLoggerLevelNameError' }* A value of `level` read from `SFDX_LOG_LEVEL`
|
341 | * was invalid.
|
342 | *
|
343 | * ```
|
344 | * // Sets the level from the environment or default value
|
345 | * logger.setLevel()
|
346 | *
|
347 | * // Set the level from the INFO enum
|
348 | * logger.setLevel(LoggerLevel.INFO)
|
349 | *
|
350 | * // Sets the level case-insensitively from a string value
|
351 | * logger.setLevel(Logger.getLevelByName('info'))
|
352 | * ```
|
353 | */
|
354 | setLevel(level) {
|
355 | if (level == null) {
|
356 | level = process.env.SFDX_LOG_LEVEL ? Logger.getLevelByName(process.env.SFDX_LOG_LEVEL) : Logger.DEFAULT_LEVEL;
|
357 | }
|
358 | this.bunyan.level(level);
|
359 | return this;
|
360 | }
|
361 | /**
|
362 | * Gets the underlying Bunyan logger.
|
363 | */
|
364 | // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
365 | getBunyanLogger() {
|
366 | return this.bunyan;
|
367 | }
|
368 | /**
|
369 | * Compares the requested log level with the current log level. Returns true if
|
370 | * the requested log level is greater than or equal to the current log level.
|
371 | *
|
372 | * @param level The requested log level to compare against the currently set log level.
|
373 | */
|
374 | shouldLog(level) {
|
375 | if (typeof level === 'string') {
|
376 | level = Bunyan.levelFromName(level);
|
377 | }
|
378 | return level >= this.getLevel();
|
379 | }
|
380 | /**
|
381 | * Use in-memory logging for this logger instance instead of any parent streams. Useful for testing.
|
382 | * For convenience this object is returned.
|
383 | *
|
384 | * **WARNING: This cannot be undone for this logger instance.**
|
385 | */
|
386 | useMemoryLogging() {
|
387 | this.bunyan.streams = [];
|
388 | this.bunyan.ringBuffer = new Bunyan.RingBuffer({ limit: 5000 });
|
389 | this.addStream({
|
390 | type: 'raw',
|
391 | stream: this.bunyan.ringBuffer,
|
392 | level: this.bunyan.level(),
|
393 | });
|
394 | return this;
|
395 | }
|
396 | /**
|
397 | * Gets an array of log line objects. Each element is an object that corresponds to a log line.
|
398 | */
|
399 | getBufferedRecords() {
|
400 | if (this.bunyan.ringBuffer) {
|
401 | return this.bunyan.ringBuffer.records;
|
402 | }
|
403 | return [];
|
404 | }
|
405 | /**
|
406 | * Reads a text blob of all the log lines contained in memory or the log file.
|
407 | */
|
408 | readLogContentsAsText() {
|
409 | if (this.bunyan.ringBuffer) {
|
410 | return this.getBufferedRecords().reduce((accum, line) => {
|
411 | accum += JSON.stringify(line) + os.EOL;
|
412 | return accum;
|
413 | }, '');
|
414 | }
|
415 | else {
|
416 | let content = '';
|
417 | // No bunyan typings
|
418 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
|
419 | this.bunyan.streams.forEach(async (stream) => {
|
420 | if (stream.type === 'file') {
|
421 | content += await fs.promises.readFile(stream.path, 'utf8');
|
422 | }
|
423 | });
|
424 | return content;
|
425 | }
|
426 | }
|
427 | /**
|
428 | * Adds a filter to be applied to all logged messages.
|
429 | *
|
430 | * @param filter A function with signature `(...args: any[]) => any[]` that transforms log message arguments.
|
431 | */
|
432 | addFilter(filter) {
|
433 | // eslint disable-line @typescript-eslint/no-explicit-any
|
434 | if (!this.bunyan.filters) {
|
435 | this.bunyan.filters = [];
|
436 | }
|
437 | this.bunyan.filters.push(filter);
|
438 | }
|
439 | /**
|
440 | * Close the logger, including any streams, and remove all listeners.
|
441 | *
|
442 | * @param fn A function with signature `(stream: LoggerStream) => void` to call for each stream with
|
443 | * the stream as an arg.
|
444 | */
|
445 | close(fn) {
|
446 | if (this.bunyan.streams) {
|
447 | try {
|
448 | this.bunyan.streams.forEach((entry) => {
|
449 | if (fn) {
|
450 | fn(entry);
|
451 | }
|
452 | // close file streams, flush buffer to disk
|
453 | // eslint-disable-next-line @typescript-eslint/unbound-method
|
454 | if (entry.type === 'file' && entry.stream && (0, ts_types_1.isFunction)(entry.stream.end)) {
|
455 | entry.stream.end();
|
456 | }
|
457 | });
|
458 | }
|
459 | finally {
|
460 | Logger.lifecycle.removeListener('uncaughtException', this.uncaughtExceptionHandler);
|
461 | Logger.lifecycle.removeListener('exit', this.exitHandler);
|
462 | }
|
463 | }
|
464 | }
|
465 | /**
|
466 | * Create a child logger, typically to add a few log record fields. For convenience this object is returned.
|
467 | *
|
468 | * @param name The name of the child logger that is emitted w/ log line as `log:<name>`.
|
469 | * @param fields Additional fields included in all log lines for the child logger.
|
470 | */
|
471 | child(name, fields = {}) {
|
472 | if (!name) {
|
473 | throw new sfError_1.SfError('LoggerNameRequired');
|
474 | }
|
475 | fields.log = name;
|
476 | const child = new Logger(name);
|
477 | // only support including additional fields on log line (no config)
|
478 | child.bunyan = this.bunyan.child(fields, true);
|
479 | child.bunyan.name = name;
|
480 | child.bunyan.filters = this.bunyan.filters;
|
481 | this.trace(`Setup child '${name}' logger instance`);
|
482 | return child;
|
483 | }
|
484 | /**
|
485 | * Add a field to all log lines for this logger. For convenience `this` object is returned.
|
486 | *
|
487 | * @param name The name of the field to add.
|
488 | * @param value The value of the field to be logged.
|
489 | */
|
490 | addField(name, value) {
|
491 | this.bunyan.fields[name] = value;
|
492 | return this;
|
493 | }
|
494 | /**
|
495 | * Logs at `trace` level with filtering applied. For convenience `this` object is returned.
|
496 | *
|
497 | * @param args Any number of arguments to be logged.
|
498 | */
|
499 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
|
500 | trace(...args) {
|
501 | this.bunyan.trace(this.applyFilters(LoggerLevel.TRACE, ...args));
|
502 | return this;
|
503 | }
|
504 | /**
|
505 | * Logs at `debug` level with filtering applied. For convenience `this` object is returned.
|
506 | *
|
507 | * @param args Any number of arguments to be logged.
|
508 | */
|
509 | debug(...args) {
|
510 | this.bunyan.debug(this.applyFilters(LoggerLevel.DEBUG, ...args));
|
511 | return this;
|
512 | }
|
513 | /**
|
514 | * Logs at `debug` level with filtering applied.
|
515 | *
|
516 | * @param cb A callback that returns on array objects to be logged.
|
517 | */
|
518 | debugCallback(cb) {
|
519 | if (this.getLevel() === LoggerLevel.DEBUG || process.env.DEBUG) {
|
520 | const result = cb();
|
521 | if ((0, ts_types_1.isArray)(result)) {
|
522 | this.bunyan.debug(this.applyFilters(LoggerLevel.DEBUG, ...result));
|
523 | }
|
524 | else {
|
525 | this.bunyan.debug(this.applyFilters(LoggerLevel.DEBUG, ...[result]));
|
526 | }
|
527 | }
|
528 | }
|
529 | /**
|
530 | * Logs at `info` level with filtering applied. For convenience `this` object is returned.
|
531 | *
|
532 | * @param args Any number of arguments to be logged.
|
533 | */
|
534 | info(...args) {
|
535 | this.bunyan.info(this.applyFilters(LoggerLevel.INFO, ...args));
|
536 | return this;
|
537 | }
|
538 | /**
|
539 | * Logs at `warn` level with filtering applied. For convenience `this` object is returned.
|
540 | *
|
541 | * @param args Any number of arguments to be logged.
|
542 | */
|
543 | warn(...args) {
|
544 | this.bunyan.warn(this.applyFilters(LoggerLevel.WARN, ...args));
|
545 | return this;
|
546 | }
|
547 | /**
|
548 | * Logs at `error` level with filtering applied. For convenience `this` object is returned.
|
549 | *
|
550 | * @param args Any number of arguments to be logged.
|
551 | */
|
552 | error(...args) {
|
553 | this.bunyan.error(this.applyFilters(LoggerLevel.ERROR, ...args));
|
554 | return this;
|
555 | }
|
556 | /**
|
557 | * Logs at `fatal` level with filtering applied. For convenience `this` object is returned.
|
558 | *
|
559 | * @param args Any number of arguments to be logged.
|
560 | */
|
561 | fatal(...args) {
|
562 | // always show fatal to stderr
|
563 | // eslint-disable-next-line no-console
|
564 | console.error(...args);
|
565 | this.bunyan.fatal(this.applyFilters(LoggerLevel.FATAL, ...args));
|
566 | return this;
|
567 | }
|
568 | /**
|
569 | * Enables logging to stdout when the DEBUG environment variable is used. It uses the logger
|
570 | * name as the debug name, so you can do DEBUG=<logger-name> to filter the results to your logger.
|
571 | */
|
572 | enableDEBUG() {
|
573 | // The debug library does this for you, but no point setting up the stream if it isn't there
|
574 | if (process.env.DEBUG && !this.debugEnabled) {
|
575 | const debuggers = {};
|
576 | debuggers.core = Debug(`${this.getName()}:core`);
|
577 | this.addStream({
|
578 | name: 'debug',
|
579 | stream: new stream_1.Writable({
|
580 | write: (chunk, encoding, next) => {
|
581 | try {
|
582 | const json = (0, kit_1.parseJsonMap)(chunk.toString());
|
583 | const logLevel = (0, ts_types_1.ensureNumber)(json.level);
|
584 | if (this.getLevel() <= logLevel) {
|
585 | let debuggerName = 'core';
|
586 | if ((0, ts_types_1.isString)(json.log)) {
|
587 | debuggerName = json.log;
|
588 | if (!debuggers[debuggerName]) {
|
589 | debuggers[debuggerName] = Debug(`${this.getName()}:${debuggerName}`);
|
590 | }
|
591 | }
|
592 | const level = LoggerLevel[logLevel];
|
593 | (0, ts_types_1.ensure)(debuggers[debuggerName])(`${level} ${json.msg}`);
|
594 | }
|
595 | }
|
596 | catch (err) {
|
597 | // do nothing
|
598 | }
|
599 | next();
|
600 | },
|
601 | }),
|
602 | // Consume all levels
|
603 | level: 0,
|
604 | });
|
605 | this.debugEnabled = true;
|
606 | }
|
607 | }
|
608 | applyFilters(logLevel, ...args) {
|
609 | if (this.shouldLog(logLevel)) {
|
610 | // No bunyan typings
|
611 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
|
612 | this.bunyan.filters.forEach((filter) => (args = filter(...args)));
|
613 | }
|
614 | return args && args.length === 1 ? args[0] : args;
|
615 | }
|
616 | createLogFmtFormatterStream(loggerStream) {
|
617 | const logFmtWriteableStream = new stream_1.Writable({
|
618 | write: (chunk, enc, cb) => {
|
619 | try {
|
620 | const parsedJSON = JSON.parse(chunk.toString());
|
621 | const keys = Object.keys(parsedJSON);
|
622 | let logEntry = '';
|
623 | keys.forEach((key) => {
|
624 | let logMsg = `${parsedJSON[key]}`;
|
625 | if (logMsg.trim().includes(' ')) {
|
626 | logMsg = `"${logMsg}"`;
|
627 | }
|
628 | logEntry += `${key}=${logMsg} `;
|
629 | });
|
630 | if (loggerStream.stream) {
|
631 | loggerStream.stream.write(logEntry.trimRight() + '\n');
|
632 | }
|
633 | }
|
634 | catch (error) {
|
635 | if (loggerStream.stream) {
|
636 | loggerStream.stream.write(chunk.toString());
|
637 | }
|
638 | }
|
639 | cb(null);
|
640 | },
|
641 | });
|
642 | return Object.assign({}, loggerStream, { stream: logFmtWriteableStream });
|
643 | }
|
644 | }
|
645 | exports.Logger = Logger;
|
646 | /**
|
647 | * The name of the root sfdx `Logger`.
|
648 | */
|
649 | Logger.ROOT_NAME = 'sf';
|
650 | /**
|
651 | * The default `LoggerLevel` when constructing new `Logger` instances.
|
652 | */
|
653 | Logger.DEFAULT_LEVEL = LoggerLevel.WARN;
|
654 | /**
|
655 | * A list of all lower case `LoggerLevel` names.
|
656 | *
|
657 | * **See** {@link LoggerLevel}
|
658 | */
|
659 | Logger.LEVEL_NAMES = Object.values(LoggerLevel)
|
660 | .filter(ts_types_1.isString)
|
661 | .map((v) => v.toLowerCase());
|
662 | // Rollup all instance-specific process event listeners together to prevent global `MaxListenersExceededWarning`s.
|
663 | Logger.lifecycle = (() => {
|
664 | const events = new events_1.EventEmitter();
|
665 | events.setMaxListeners(0); // never warn on listener counts
|
666 | process.on('uncaughtException', (err) => events.emit('uncaughtException', err));
|
667 | process.on('exit', () => events.emit('exit'));
|
668 | return events;
|
669 | })();
|
670 | // Ok to log clientid
|
671 | const FILTERED_KEYS = [
|
672 | 'sid',
|
673 | 'Authorization',
|
674 | // Any json attribute that contains the words "access" and "token" will have the attribute/value hidden
|
675 | { name: 'access_token', regex: 'access[^\'"]*token' },
|
676 | // Any json attribute that contains the words "refresh" and "token" will have the attribute/value hidden
|
677 | { name: 'refresh_token', regex: 'refresh[^\'"]*token' },
|
678 | 'clientsecret',
|
679 | // Any json attribute that contains the words "sfdx", "auth", and "url" will have the attribute/value hidden
|
680 | { name: 'sfdxauthurl', regex: 'sfdx[^\'"]*auth[^\'"]*url' },
|
681 | ];
|
682 | // SFDX code and plugins should never show tokens or connect app information in the logs
|
683 | const _filter = (...args) => {
|
684 | return args.map((arg) => {
|
685 | if ((0, ts_types_1.isArray)(arg)) {
|
686 | return _filter(...arg);
|
687 | }
|
688 | if (arg) {
|
689 | let _arg;
|
690 | // Normalize all objects into a string. This include errors.
|
691 | if (arg instanceof Buffer) {
|
692 | _arg = '<Buffer>';
|
693 | }
|
694 | else if ((0, ts_types_1.isObject)(arg)) {
|
695 | _arg = JSON.stringify(arg);
|
696 | }
|
697 | else if ((0, ts_types_1.isString)(arg)) {
|
698 | _arg = arg;
|
699 | }
|
700 | else {
|
701 | _arg = '';
|
702 | }
|
703 | const HIDDEN = 'HIDDEN';
|
704 | FILTERED_KEYS.forEach((key) => {
|
705 | let expElement = key;
|
706 | let expName = key;
|
707 | // Filtered keys can be strings or objects containing regular expression components.
|
708 | if ((0, ts_types_1.isPlainObject)(key)) {
|
709 | expElement = key.regex;
|
710 | expName = key.name;
|
711 | }
|
712 | const hiddenAttrMessage = `"<${expName} - ${HIDDEN}>"`;
|
713 | // Match all json attribute values case insensitive: ex. {" Access*^&(*()^* Token " : " 45143075913458901348905 \n\t" ...}
|
714 | const regexTokens = new RegExp(`(['"][^'"]*${expElement}[^'"]*['"]\\s*:\\s*)['"][^'"]*['"]`, 'gi');
|
715 | _arg = _arg.replace(regexTokens, `$1${hiddenAttrMessage}`);
|
716 | // Match all key value attribute case insensitive: ex. {" key\t" : ' access_token ' , " value " : " dsafgasr431 " ....}
|
717 | const keyRegex = new RegExp(`(['"]\\s*key\\s*['"]\\s*:)\\s*['"]\\s*${expElement}\\s*['"]\\s*.\\s*['"]\\s*value\\s*['"]\\s*:\\s*['"]\\s*[^'"]*['"]`, 'gi');
|
718 | _arg = _arg.replace(keyRegex, `$1${hiddenAttrMessage}`);
|
719 | });
|
720 | _arg = _arg.replace(/(00D\w{12,15})![.\w]*/, `<${HIDDEN}>`);
|
721 | // return an object if an object was logged; otherwise return the filtered string.
|
722 | return (0, ts_types_1.isObject)(arg) ? (0, kit_1.parseJson)(_arg) : _arg;
|
723 | }
|
724 | else {
|
725 | return arg;
|
726 | }
|
727 | });
|
728 | };
|
729 | //# sourceMappingURL=logger.js.map |
\ | No newline at end of file |