UNPKG

22.1 kBJavaScriptView Raw
1"use strict";
2/*
3 * Copyright (c) 2018, salesforce.com, inc.
4 * All rights reserved.
5 * SPDX-License-Identifier: BSD-3-Clause
6 * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
7 */
8Object.defineProperty(exports, "__esModule", { value: true });
9// tslint:disable-next-line:ordered-imports
10// @ts-ignore No typings available for our copy of bunyan
11const Bunyan = require("@salesforce/bunyan");
12const kit_1 = require("@salesforce/kit");
13const ts_types_1 = require("@salesforce/ts-types");
14const Debug = require("debug");
15const events_1 = require("events");
16const os = require("os");
17const path = require("path");
18const stream_1 = require("stream");
19const global_1 = require("./global");
20const sfdxError_1 = require("./sfdxError");
21const fs_1 = require("./util/fs");
22/**
23 * Standard `Logger` levels.
24 *
25 * **See** {@link https://github.com/forcedotcom/node-bunyan#levels|Bunyan Levels}
26 */
27var 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 * A logging abstraction powered by {@link https://github.com/forcedotcom/node-bunyan|Bunyan} that provides both a default
38 * logger configuration that will log to `sfdx.log`, and a way to create custom loggers based on the same foundation.
39 *
40 * ```
41 * // Gets the root sfdx logger
42 * const logger = await Logger.root();
43 *
44 * // Creates a child logger of the root sfdx logger with custom fields applied
45 * const childLogger = await Logger.child('myRootChild', {tag: 'value'});
46 *
47 * // Creates a custom logger unaffiliated with the root logger
48 * const myCustomLogger = new Logger('myCustomLogger');
49 *
50 * // Creates a child of a custom logger unaffiliated with the root logger with custom fields applied
51 * const myCustomChildLogger = myCustomLogger.child('myCustomChild', {tag: 'value'});
52 * ```
53 * **See** https://github.com/forcedotcom/node-bunyan
54 *
55 * **See** https://developer.salesforce.com/docs/atlas.en-us.sfdx_dev.meta/sfdx_dev/sfdx_dev_cli_log_messages.htm
56 */
57class Logger {
58 /**
59 * Constructs a new `Logger`.
60 *
61 * @param optionsOrName A set of `LoggerOptions` or name to use with the default options.
62 *
63 * **Throws** *{@link SfdxError}{ name: 'RedundantRootLogger' }* More than one attempt is made to construct the root
64 * `Logger`.
65 */
66 constructor(optionsOrName) {
67 this.uncaughtExceptionHandler = (err) => {
68 // W-7558552
69 // Only log uncaught exceptions in root logger
70 if (this === Logger.rootLogger) {
71 // log the exception
72 // FIXME: good chance this won't be logged because
73 // process.exit was called before this is logged
74 // https://github.com/trentm/node-bunyan/issues/95
75 this.fatal(err);
76 }
77 };
78 this.exitHandler = () => {
79 this.close();
80 };
81 let options;
82 if (typeof optionsOrName === 'string') {
83 options = {
84 name: optionsOrName,
85 level: Logger.DEFAULT_LEVEL,
86 serializers: Bunyan.stdSerializers
87 };
88 }
89 else {
90 options = optionsOrName;
91 }
92 if (Logger.rootLogger && options.name === Logger.ROOT_NAME) {
93 throw new sfdxError_1.SfdxError('RedundantRootLogger');
94 }
95 this.bunyan = new Bunyan(options);
96 this.bunyan.name = options.name;
97 this.bunyan.filters = [];
98 this.bunyan.streams = [];
99 // all SFDX loggers must filter sensitive data
100 this.addFilter((...args) => _filter(...args));
101 if (global_1.Global.getEnvironmentMode() !== global_1.Mode.TEST) {
102 Logger.lifecycle.on('uncaughtException', this.uncaughtExceptionHandler);
103 Logger.lifecycle.on('exit', this.exitHandler);
104 }
105 this.trace(`Created '${this.getName()}' logger instance`);
106 }
107 /**
108 * Gets the root logger with the default level and file stream.
109 */
110 static async root() {
111 if (this.rootLogger) {
112 return this.rootLogger;
113 }
114 const rootLogger = (this.rootLogger = new Logger(Logger.ROOT_NAME).setLevel());
115 // disable log file writing, if applicable
116 if (process.env.SFDX_DISABLE_LOG_FILE !== 'true' && global_1.Global.getEnvironmentMode() !== global_1.Mode.TEST) {
117 await rootLogger.addLogFileStream(global_1.Global.LOG_FILE_PATH);
118 }
119 // The debug library does this for you, but no point setting up the stream if it isn't there
120 if (process.env.DEBUG) {
121 const debuggers = {};
122 debuggers.core = Debug(`${rootLogger.getName()}:core`);
123 rootLogger.addStream({
124 name: 'debug',
125 stream: new stream_1.Writable({
126 write: (chunk, encoding, next) => {
127 const json = kit_1.parseJsonMap(chunk.toString());
128 let debuggerName = 'core';
129 if (ts_types_1.isString(json.log)) {
130 debuggerName = json.log;
131 if (!debuggers[debuggerName]) {
132 debuggers[debuggerName] = Debug(`${rootLogger.getName()}:${debuggerName}`);
133 }
134 }
135 const level = LoggerLevel[ts_types_1.ensureNumber(json.level)];
136 ts_types_1.ensure(debuggers[debuggerName])(`${level} ${json.msg}`);
137 next();
138 }
139 }),
140 // Consume all levels
141 level: 0
142 });
143 }
144 return rootLogger;
145 }
146 /**
147 * Destroys the root `Logger`.
148 *
149 * @ignore
150 */
151 static destroyRoot() {
152 if (this.rootLogger) {
153 this.rootLogger.close();
154 this.rootLogger = undefined;
155 }
156 }
157 /**
158 * Create a child of the root logger, inheriting this instance's configuration such as `level`, `streams`, etc.
159 *
160 * @param name The name of the child logger.
161 * @param fields Additional fields included in all log lines.
162 */
163 static async child(name, fields) {
164 return (await Logger.root()).child(name, fields);
165 }
166 /**
167 * Gets a numeric `LoggerLevel` value by string name.
168 *
169 * @param {string} levelName The level name to convert to a `LoggerLevel` enum value.
170 *
171 * **Throws** *{@link SfdxError}{ name: 'UnrecognizedLoggerLevelName' }* The level name was not case-insensitively recognized as a valid `LoggerLevel` value.
172 * @see {@Link LoggerLevel}
173 */
174 static getLevelByName(levelName) {
175 levelName = levelName.toUpperCase();
176 if (!ts_types_1.isKeyOf(LoggerLevel, levelName)) {
177 throw new sfdxError_1.SfdxError('UnrecognizedLoggerLevelName');
178 }
179 return LoggerLevel[levelName];
180 }
181 /**
182 * Adds a stream.
183 *
184 * @param stream The stream configuration to add.
185 * @param defaultLevel The default level of the stream.
186 */
187 addStream(stream, defaultLevel) {
188 this.bunyan.addStream(stream, defaultLevel);
189 }
190 /**
191 * Adds a file stream to this logger. Resolved or rejected upon completion of the addition.
192 *
193 * @param logFile The path to the log file. If it doesn't exist it will be created.
194 */
195 async addLogFileStream(logFile) {
196 try {
197 // Check if we have write access to the log file (i.e., we created it already)
198 await fs_1.fs.access(logFile, fs_1.fs.constants.W_OK);
199 }
200 catch (err1) {
201 try {
202 await fs_1.fs.mkdirp(path.dirname(logFile), {
203 mode: fs_1.fs.DEFAULT_USER_DIR_MODE
204 });
205 }
206 catch (err2) {
207 // noop; directory exists already
208 }
209 try {
210 await fs_1.fs.writeFile(logFile, '', { mode: fs_1.fs.DEFAULT_USER_FILE_MODE });
211 }
212 catch (err3) {
213 throw sfdxError_1.SfdxError.wrap(err3);
214 }
215 }
216 // avoid multiple streams to same log file
217 if (!this.bunyan.streams.find(
218 // tslint:disable-next-line:no-any No bunyan typings
219 (stream) => stream.type === 'file' && stream.path === logFile)) {
220 // TODO: rotating-file
221 // https://github.com/trentm/node-bunyan#stream-type-rotating-file
222 this.addStream({
223 type: 'file',
224 path: logFile,
225 level: this.bunyan.level()
226 });
227 }
228 }
229 /**
230 * Gets the name of this logger.
231 */
232 getName() {
233 return this.bunyan.name;
234 }
235 /**
236 * Gets the current level of this logger.
237 */
238 getLevel() {
239 return this.bunyan.level();
240 }
241 /**
242 * Set the logging level of all streams for this logger. If a specific `level` is not provided, this method will
243 * attempt to read it from the environment variable `SFDX_LOG_LEVEL`, and if not found,
244 * {@link Logger.DEFAULT_LOG_LEVEL} will be used instead. For convenience `this` object is returned.
245 *
246 * @param {LoggerLevelValue} [level] The logger level.
247 *
248 * **Throws** *{@link SfdxError}{ name: 'UnrecognizedLoggerLevelName' }* A value of `level` read from `SFDX_LOG_LEVEL`
249 * was invalid.
250 *
251 * ```
252 * // Sets the level from the environment or default value
253 * logger.setLevel()
254 *
255 * // Set the level from the INFO enum
256 * logger.setLevel(LoggerLevel.INFO)
257 *
258 * // Sets the level case-insensitively from a string value
259 * logger.setLevel(Logger.getLevelByName('info'))
260 * ```
261 */
262 setLevel(level) {
263 if (level == null) {
264 level = process.env.SFDX_LOG_LEVEL ? Logger.getLevelByName(process.env.SFDX_LOG_LEVEL) : Logger.DEFAULT_LEVEL;
265 }
266 this.bunyan.level(level);
267 return this;
268 }
269 /**
270 * Gets the underlying Bunyan logger.
271 */
272 // tslint:disable-next-line:no-any
273 getBunyanLogger() {
274 return this.bunyan;
275 }
276 /**
277 * Compares the requested log level with the current log level. Returns true if
278 * the requested log level is greater than or equal to the current log level.
279 *
280 * @param level The requested log level to compare against the currently set log level.
281 */
282 shouldLog(level) {
283 if (typeof level === 'string') {
284 level = Bunyan.levelFromName(level);
285 }
286 return level >= this.getLevel();
287 }
288 /**
289 * Use in-memory logging for this logger instance instead of any parent streams. Useful for testing.
290 * For convenience this object is returned.
291 *
292 * **WARNING: This cannot be undone for this logger instance.**
293 */
294 useMemoryLogging() {
295 this.bunyan.streams = [];
296 this.bunyan.ringBuffer = new Bunyan.RingBuffer({ limit: 5000 });
297 this.addStream({
298 type: 'raw',
299 stream: this.bunyan.ringBuffer,
300 level: this.bunyan.level()
301 });
302 return this;
303 }
304 /**
305 * Gets an array of log line objects. Each element is an object that corresponds to a log line.
306 */
307 getBufferedRecords() {
308 if (this.bunyan.ringBuffer) {
309 return this.bunyan.ringBuffer.records;
310 }
311 return [];
312 }
313 /**
314 * Reads a text blob of all the log lines contained in memory or the log file.
315 */
316 readLogContentsAsText() {
317 if (this.bunyan.ringBuffer) {
318 return this.getBufferedRecords().reduce((accum, line) => {
319 accum += JSON.stringify(line) + os.EOL;
320 return accum;
321 }, '');
322 }
323 else {
324 let content = '';
325 // tslint:disable-next-line:no-any No bunyan typings
326 this.bunyan.streams.forEach(async (stream) => {
327 if (stream.type === 'file') {
328 content += await fs_1.fs.readFile(stream.path, 'utf8');
329 }
330 });
331 return content;
332 }
333 }
334 /**
335 * Adds a filter to be applied to all logged messages.
336 *
337 * @param filter A function with signature `(...args: any[]) => any[]` that transforms log message arguments.
338 */
339 addFilter(filter) {
340 // tslint:disable-line:no-any
341 if (!this.bunyan.filters) {
342 this.bunyan.filters = [];
343 }
344 this.bunyan.filters.push(filter);
345 }
346 /**
347 * Close the logger, including any streams, and remove all listeners.
348 *
349 * @param fn A function with signature `(stream: LoggerStream) => void` to call for each stream with
350 * the stream as an arg.
351 */
352 close(fn) {
353 if (this.bunyan.streams) {
354 try {
355 this.bunyan.streams.forEach((entry) => {
356 if (fn) {
357 fn(entry);
358 }
359 // close file streams, flush buffer to disk
360 if (entry.type === 'file' && entry.stream && ts_types_1.isFunction(entry.stream.end)) {
361 entry.stream.end();
362 }
363 });
364 }
365 finally {
366 Logger.lifecycle.removeListener('uncaughtException', this.uncaughtExceptionHandler);
367 Logger.lifecycle.removeListener('exit', this.exitHandler);
368 }
369 }
370 }
371 /**
372 * Create a child logger, typically to add a few log record fields. For convenience this object is returned.
373 *
374 * @param name The name of the child logger that is emitted w/ log line as `log:<name>`.
375 * @param fields Additional fields included in all log lines for the child logger.
376 */
377 child(name, fields = {}) {
378 if (!name) {
379 throw new sfdxError_1.SfdxError('LoggerNameRequired');
380 }
381 fields.log = name;
382 const child = new Logger(name);
383 // only support including additional fields on log line (no config)
384 child.bunyan = this.bunyan.child(fields, true);
385 child.bunyan.name = name;
386 child.bunyan.filters = this.bunyan.filters;
387 this.trace(`Setup child '${name}' logger instance`);
388 return child;
389 }
390 /**
391 * Add a field to all log lines for this logger. For convenience `this` object is returned.
392 *
393 * @param name The name of the field to add.
394 * @param value The value of the field to be logged.
395 */
396 addField(name, value) {
397 this.bunyan.fields[name] = value;
398 return this;
399 }
400 /**
401 * Logs at `trace` level with filtering applied. For convenience `this` object is returned.
402 *
403 * @param args Any number of arguments to be logged.
404 */
405 // tslint:disable-next-line:no-any
406 trace(...args) {
407 this.bunyan.trace(this.applyFilters(LoggerLevel.TRACE, ...args));
408 return this;
409 }
410 /**
411 * Logs at `debug` level with filtering applied. For convenience `this` object is returned.
412 *
413 * @param args Any number of arguments to be logged.
414 */
415 debug(...args) {
416 this.bunyan.debug(this.applyFilters(LoggerLevel.DEBUG, ...args));
417 return this;
418 }
419 /**
420 * Logs at `debug` level with filtering applied.
421 *
422 * @param cb A callback that returns on array objects to be logged.
423 */
424 debugCallback(cb) {
425 if (this.getLevel() === LoggerLevel.DEBUG || process.env.DEBUG) {
426 const result = cb();
427 if (ts_types_1.isArray(result)) {
428 this.bunyan.debug(this.applyFilters(LoggerLevel.DEBUG, ...result));
429 }
430 else {
431 this.bunyan.debug(this.applyFilters(LoggerLevel.DEBUG, ...[result]));
432 }
433 }
434 }
435 /**
436 * Logs at `info` level with filtering applied. For convenience `this` object is returned.
437 *
438 * @param args Any number of arguments to be logged.
439 */
440 info(...args) {
441 this.bunyan.info(this.applyFilters(LoggerLevel.INFO, ...args));
442 return this;
443 }
444 /**
445 * Logs at `warn` level with filtering applied. For convenience `this` object is returned.
446 *
447 * @param args Any number of arguments to be logged.
448 */
449 warn(...args) {
450 this.bunyan.warn(this.applyFilters(LoggerLevel.WARN, ...args));
451 return this;
452 }
453 /**
454 * Logs at `error` level with filtering applied. For convenience `this` object is returned.
455 *
456 * @param args Any number of arguments to be logged.
457 */
458 error(...args) {
459 this.bunyan.error(this.applyFilters(LoggerLevel.ERROR, ...args));
460 return this;
461 }
462 /**
463 * Logs at `fatal` level with filtering applied. For convenience `this` object is returned.
464 *
465 * @param args Any number of arguments to be logged.
466 */
467 fatal(...args) {
468 // always show fatal to stderr
469 console.error(...args);
470 this.bunyan.fatal(this.applyFilters(LoggerLevel.FATAL, ...args));
471 return this;
472 }
473 applyFilters(logLevel, ...args) {
474 if (this.shouldLog(logLevel)) {
475 // tslint:disable-next-line:no-any No bunyan typings
476 this.bunyan.filters.forEach((filter) => (args = filter(...args)));
477 }
478 return args && args.length === 1 ? args[0] : args;
479 }
480}
481/**
482 * The name of the root sfdx `Logger`.
483 */
484Logger.ROOT_NAME = 'sfdx';
485/**
486 * The default `LoggerLevel` when constructing new `Logger` instances.
487 */
488Logger.DEFAULT_LEVEL = LoggerLevel.WARN;
489/**
490 * A list of all lower case `LoggerLevel` names.
491 *
492 * **See** {@link LoggerLevel}
493 */
494Logger.LEVEL_NAMES = Object.values(LoggerLevel)
495 .filter(ts_types_1.isString)
496 .map(v => v.toLowerCase());
497// Rollup all instance-specific process event listeners together to prevent global `MaxListenersExceededWarning`s.
498Logger.lifecycle = (() => {
499 const events = new events_1.EventEmitter();
500 events.setMaxListeners(0); // never warn on listener counts
501 process.on('uncaughtException', err => events.emit('uncaughtException', err));
502 process.on('exit', () => events.emit('exit'));
503 return events;
504})();
505exports.Logger = Logger;
506// Ok to log clientid
507const FILTERED_KEYS = [
508 'sid',
509 'Authorization',
510 // Any json attribute that contains the words "access" and "token" will have the attribute/value hidden
511 { name: 'access_token', regex: 'access[^\'"]*token' },
512 // Any json attribute that contains the words "refresh" and "token" will have the attribute/value hidden
513 { name: 'refresh_token', regex: 'refresh[^\'"]*token' },
514 'clientsecret',
515 // Any json attribute that contains the words "sfdx", "auth", and "url" will have the attribute/value hidden
516 { name: 'sfdxauthurl', regex: 'sfdx[^\'"]*auth[^\'"]*url' }
517];
518// SFDX code and plugins should never show tokens or connect app information in the logs
519const _filter = (...args) => {
520 return args.map(arg => {
521 if (ts_types_1.isArray(arg)) {
522 return _filter(...arg);
523 }
524 if (arg) {
525 let _arg;
526 // Normalize all objects into a string. This include errors.
527 if (arg instanceof Buffer) {
528 _arg = '<Buffer>';
529 }
530 else if (ts_types_1.isObject(arg)) {
531 _arg = JSON.stringify(arg);
532 }
533 else if (ts_types_1.isString(arg)) {
534 _arg = arg;
535 }
536 else {
537 _arg = '';
538 }
539 const HIDDEN = 'HIDDEN';
540 FILTERED_KEYS.forEach((key) => {
541 let expElement = key;
542 let expName = key;
543 // Filtered keys can be strings or objects containing regular expression components.
544 if (ts_types_1.isPlainObject(key)) {
545 expElement = key.regex;
546 expName = key.name;
547 }
548 const hiddenAttrMessage = `"<${expName} - ${HIDDEN}>"`;
549 // Match all json attribute values case insensitive: ex. {" Access*^&(*()^* Token " : " 45143075913458901348905 \n\t" ...}
550 const regexTokens = new RegExp(`(['"][^'"]*${expElement}[^'"]*['"]\\s*:\\s*)['"][^'"]*['"]`, 'gi');
551 _arg = _arg.replace(regexTokens, `$1${hiddenAttrMessage}`);
552 // Match all key value attribute case insensitive: ex. {" key\t" : ' access_token ' , " value " : " dsafgasr431 " ....}
553 const keyRegex = new RegExp(`(['"]\\s*key\\s*['"]\\s*:)\\s*['"]\\s*${expElement}\\s*['"]\\s*.\\s*['"]\\s*value\\s*['"]\\s*:\\s*['"]\\s*[^'"]*['"]`, 'gi');
554 _arg = _arg.replace(keyRegex, `$1${hiddenAttrMessage}`);
555 });
556 // This is a jsforce message we are masking. This can be removed after the following pull request is committed
557 // and pushed to a jsforce release.
558 //
559 // Looking For: "Refreshed access token = ..."
560 // Related Jsforce pull requests:
561 // https://github.com/jsforce/jsforce/pull/598
562 // https://github.com/jsforce/jsforce/pull/608
563 // https://github.com/jsforce/jsforce/pull/609
564 const jsForceTokenRefreshRegEx = new RegExp('Refreshed(.*)access(.*)token(.*)=\\s*[^\'"\\s*]*');
565 _arg = _arg.replace(jsForceTokenRefreshRegEx, `<refresh_token - ${HIDDEN}>`);
566 _arg = _arg.replace(/sid=(.*)/, `sid=<${HIDDEN}>`);
567 // return an object if an object was logged; otherwise return the filtered string.
568 return ts_types_1.isObject(arg) ? kit_1.parseJson(_arg) : _arg;
569 }
570 else {
571 return arg;
572 }
573 });
574};
575//# sourceMappingURL=logger.js.map
\No newline at end of file