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