-
Notifications
You must be signed in to change notification settings - Fork 9.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
misc(logger): 1.3.0, package types, timeDecorate, add tests #6740
Changes from 5 commits
37270f2
c25a48e
7979cc1
95d9d96
9df9f7d
722d955
e5e1f5c
0cd2951
e28f037
eba785a
93a8afa
c4d5a08
6bf64d3
2ea9423
2001a5c
b2c7dab
83c60f7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
/** | ||
* @license Copyright 2018 Google Inc. All Rights Reserved. | ||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 | ||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. | ||
*/ | ||
'use strict'; | ||
|
||
/** @type {typeof import('../../lighthouse-logger')} */ | ||
module.exports = require('lighthouse-logger'); | ||
Original file line number | Diff line number | Diff line change | ||||||
---|---|---|---|---|---|---|---|---|
@@ -1,17 +1,59 @@ | ||||||||
/** | ||||||||
* @license Copyright 2016 Google Inc. All Rights Reserved. | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure what the policy is here, we haven't historically updated years because we touched the file, but it conceptually makes sense to do @brendankenny you're our usual license police :) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. legally it doesnt matter one bit, so let's leave it back on 2016. seems like our rough policy is use current year when introducing new files. |
||||||||
* @license Copyright 2018 Google Inc. All Rights Reserved. | ||||||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 | ||||||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. | ||||||||
*/ | ||||||||
'use strict'; | ||||||||
|
||||||||
const debug = require('debug'); | ||||||||
// @ts-ignore | ||||||||
const marky = require('marky'); | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can you add a type file for There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yah?
or whatever of the many variations that would work in |
||||||||
|
||||||||
const EventEmitter = require('events').EventEmitter; | ||||||||
|
||||||||
/** | ||||||||
* @template {Function} F | ||||||||
* @typedef {import('./type-helpers').ArgumentTypes<F>} ArgumentTypes | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think you can get away with just the built-in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Great! |
||||||||
*/ | ||||||||
|
||||||||
/** | ||||||||
* @template T | ||||||||
* @typedef {import('./type-helpers').IsFunction<T>} IsFunction | ||||||||
*/ | ||||||||
|
||||||||
/** | ||||||||
* @template Class | ||||||||
* @template {*[]} Args | ||||||||
* @typedef {{ | ||||||||
* msg: string | ((this: Class, ...args: Args) => string), | ||||||||
* id?: string | ((this: Class, ...args: Args) => string), | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. what if we drop the dynamic messages and only support strings here (at least for now)? It would make this file simpler to read, simpler for type checking, and reduces the non-local reasoning needed where the AFAICT, the only places we need dynamically created strings are for
Due to the structure of And then at a later date we can always add support if we find we want it. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It'll be most immediately needed by #6410, since each call to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. hmm, yeah, that's useful enough that I guess we should probably keep it |
||||||||
* timeStartLogLevel?: LogAction, | ||||||||
* timeEndLogLevel?: LogAction, | ||||||||
* }} TimeDecorateOpts | ||||||||
*/ | ||||||||
|
||||||||
/** | ||||||||
* @typedef {'verbose'|'error'|'silent'|'log'|'info'|'warn'} LogLevel | ||||||||
*/ | ||||||||
/** | ||||||||
* @typedef {LogLevel & keyof typeof Log} LogAction | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this is probably a little too clever to be able to quickly see what's going on :) I'd say at least have But really |
||||||||
*/ | ||||||||
|
||||||||
/** | ||||||||
* @typedef {{ | ||||||||
* msg:string, | ||||||||
* id: string, | ||||||||
* args?: any[], | ||||||||
* }} Status | ||||||||
*/ | ||||||||
|
||||||||
/** | ||||||||
* @typedef {[any, ...any[]]} ArrayAtLeastOne | ||||||||
*/ | ||||||||
|
||||||||
const isWindows = process.platform === 'win32'; | ||||||||
|
||||||||
// process.browser is set when browserify'd via the `process` npm module | ||||||||
// @ts-ignore | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. combine the comments? ( |
||||||||
const isBrowser = process.browser; | ||||||||
|
||||||||
const colors = { | ||||||||
|
@@ -24,6 +66,7 @@ const colors = { | |||||||
}; | ||||||||
|
||||||||
// whitelist non-red/yellow colors for debug() | ||||||||
// @ts-ignore | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. combine comments (and maybe add something about it being non-public to explain the ts-ignore) |
||||||||
debug.colors = [colors.cyan, colors.green, colors.blue, colors.magenta]; | ||||||||
|
||||||||
class Emitter extends EventEmitter { | ||||||||
|
@@ -50,33 +93,46 @@ class Emitter extends EventEmitter { | |||||||
} | ||||||||
} | ||||||||
|
||||||||
/** @type {{[k: string] : debug.IDebugger}} */ | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: can equivalently do |
||||||||
const loggersByTitle = {}; | ||||||||
|
||||||||
const loggingBufferColumns = 25; | ||||||||
|
||||||||
/** @type {LogLevel} */ | ||||||||
let level_; | ||||||||
|
||||||||
class Log { | ||||||||
/** | ||||||||
* @param {string} title | ||||||||
* @param {ArrayAtLeastOne} argsArray | ||||||||
*/ | ||||||||
static _logToStdErr(title, argsArray) { | ||||||||
const log = Log.loggerfn(title); | ||||||||
log(...argsArray); | ||||||||
} | ||||||||
|
||||||||
/** | ||||||||
* @param {string} title | ||||||||
*/ | ||||||||
static loggerfn(title) { | ||||||||
let log = loggersByTitle[title]; | ||||||||
if (!log) { | ||||||||
log = debug(title); | ||||||||
loggersByTitle[title] = log; | ||||||||
// errors with red, warnings with yellow. | ||||||||
if (title.endsWith('error')) { | ||||||||
// @ts-ignore | ||||||||
log.color = colors.red; | ||||||||
} else if (title.endsWith('warn')) { | ||||||||
// @ts-ignore | ||||||||
log.color = colors.yellow; | ||||||||
} | ||||||||
} | ||||||||
return log; | ||||||||
} | ||||||||
|
||||||||
/** | ||||||||
* @param {string} level | ||||||||
* @param {LogLevel} level | ||||||||
*/ | ||||||||
static setLevel(level) { | ||||||||
level_ = level; | ||||||||
|
@@ -99,10 +155,10 @@ class Log { | |||||||
* A simple formatting utility for event logging. | ||||||||
* @param {string} prefix | ||||||||
* @param {!Object} data A JSON-serializable object of event data to log. | ||||||||
* @param {string=} level Optional logging level. Defaults to 'log'. | ||||||||
* @param {LogLevel=} level Optional logging level. Defaults to 'log'. | ||||||||
*/ | ||||||||
static formatProtocol(prefix, data, level) { | ||||||||
const columns = (!process || process.browser) ? Infinity : process.stdout.columns; | ||||||||
const columns = isBrowser ? Infinity : (process.stdout.columns || Infinity); | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nice one |
||||||||
const method = data.method || '?????'; | ||||||||
const maxLength = columns - method.length - prefix.length - loggingBufferColumns; | ||||||||
// IO.read blacklisted here to avoid logging megabytes of trace data | ||||||||
|
@@ -118,33 +174,164 @@ class Log { | |||||||
return level_ === 'verbose'; | ||||||||
} | ||||||||
|
||||||||
/** | ||||||||
* @param {Status} param0 | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. maybe s/param0/status? |
||||||||
* @param {LogAction} level | ||||||||
*/ | ||||||||
static time({msg, id, args = []}, level = 'log') { | ||||||||
marky.mark(id); | ||||||||
Log[level]('status', msg, ...args); | ||||||||
} | ||||||||
|
||||||||
/** | ||||||||
* @param {Status} param0 | ||||||||
* @param {LogAction} level | ||||||||
*/ | ||||||||
static timeEnd({msg, id, args = []}, level = 'verbose') { | ||||||||
Log[level]('statusEnd', msg, ...args); | ||||||||
marky.stop(id); | ||||||||
} | ||||||||
|
||||||||
/* eslint-disable no-invalid-this */ | ||||||||
/** | ||||||||
* Decorates a function, calling time/timeEnd before/after calling the original function. | ||||||||
* @template T, R | ||||||||
* @template {*[]} Args | ||||||||
* @param {(this: T, ...args: Args) => R} originalFn | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. hmm, it seems from the uses in #6515 and What about passing in a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think that is an option with how anyhow, none of the uses cases in #6515 required |
||||||||
* @param {TimeDecorateOpts<T, Args>} opts | ||||||||
* @return {(this: T, ...args: Args) => R} | ||||||||
*/ | ||||||||
static timeDecorate(originalFn, opts) { | ||||||||
/** | ||||||||
* @type {(this: *, ...args: *[]) => string} | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||||||||
*/ | ||||||||
const computeMsg = (_this, args) => { | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. could this fn be as simple as: const computeMsg = (_this, args) => {
if (typeof opts.msg === 'string') return opts.msg;
if (typeof opts.msg === 'function') return opts.msg.apply(_this, args);
throw new Error('expected msg');
}; and same with computeId() checking that the first two cases don't return something falsy seems.. unnecessary to me, but w/e There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. oh ya for sure this only made sense before I figured out how to strongly type all this. had to verify this at runtime. but now TSC does the heavy lifting here :) |
||||||||
let msg = ''; | ||||||||
if (typeof opts.msg === 'string') { | ||||||||
msg = opts.msg; | ||||||||
} else if (typeof opts.msg === 'function') { | ||||||||
// TODO turn on --strictBindCallApply when tsc is upgraded to 3.2 | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🚗 🏁 #6684 |
||||||||
msg = opts.msg.apply(_this, args); | ||||||||
} | ||||||||
if (!msg) { | ||||||||
throw new Error('expected msg'); | ||||||||
} | ||||||||
return msg; | ||||||||
}; | ||||||||
|
||||||||
/** | ||||||||
* @type {(this: *, ...args: *[]) => string} | ||||||||
*/ | ||||||||
const computeId = (_this, args) => { | ||||||||
let id = ''; | ||||||||
if (typeof opts.id === 'string') { | ||||||||
id = opts.id; | ||||||||
} else if (typeof opts.id === 'function') { | ||||||||
// TODO turn on --strictBindCallApply when tsc is upgraded to 3.2 | ||||||||
id = opts.id.apply(this, args); | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. oops! should be |
||||||||
} else { | ||||||||
id = `lh:${originalFn.name}`; | ||||||||
} | ||||||||
if (!id) { | ||||||||
throw new Error('expected id'); | ||||||||
} | ||||||||
return id; | ||||||||
}; | ||||||||
|
||||||||
/** | ||||||||
* @type {(this: T, ...args: Args) => R} | ||||||||
*/ | ||||||||
const fn = function timeDecoratedFn(...args) { | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. drop the |
||||||||
const timeStartLogLevel = opts.timeStartLogLevel || 'log'; | ||||||||
const timeEndLogLevel = opts.timeEndLogLevel || 'verbose'; | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. move outside |
||||||||
|
||||||||
const status = {msg: computeMsg(this, args), id: computeId(this, args)}; | ||||||||
Log.time(status, timeStartLogLevel); | ||||||||
|
||||||||
let result; | ||||||||
try { | ||||||||
result = originalFn.apply(this, args); | ||||||||
} catch (err) { | ||||||||
Log.timeEnd(status, timeEndLogLevel); | ||||||||
err.stack = err.stack.replace(/.* at timeDecoratedFn .*\n/g, ''); | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. add a comment to explain this hack? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ya |
||||||||
throw err; | ||||||||
} | ||||||||
|
||||||||
if (result && typeof result.then === 'function') { | ||||||||
return result.then((/** @type {any} */ value) => { | ||||||||
Log.timeEnd(status, timeEndLogLevel); | ||||||||
return value; | ||||||||
}).catch((/** @type {any} */ err) => { | ||||||||
Log.timeEnd(status, timeEndLogLevel); | ||||||||
err.stack = err.stack.replace(/.* at timeDecoratedFn .*\n/, ''); | ||||||||
throw err; | ||||||||
}); | ||||||||
} else { | ||||||||
Log.timeEnd(status, timeEndLogLevel); | ||||||||
return result; | ||||||||
} | ||||||||
}; | ||||||||
return fn; | ||||||||
} | ||||||||
/* eslint-enable no-invalid-this */ | ||||||||
|
||||||||
/** | ||||||||
* Decorates (like timeDecorate) specified methods of a class. | ||||||||
* If decorating instance methods, use the class's prototype. | ||||||||
* If decorating static methods, use the class directly. | ||||||||
* @template {Object|Function} Class | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can we be more specific? This is just There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I want to accept anything newable (classes, so basically any function) or objects (prototypes). I suppose objects and functions covers everything :P What about something silly like this? |
||||||||
* @template {keyof Class} Prop | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. should be limited to properties that are functions? E.g. you can pass a number property in (and it will accept any parameters for There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this part guards against non-functions:
|
||||||||
* @param {Class} klass | ||||||||
* @param {{[key in Prop]: TimeDecorateOpts<Class, ArgumentTypes<IsFunction<Class[key]>>>}} methods | ||||||||
*/ | ||||||||
static timeDecorateClass(klass, methods) { | ||||||||
for (const [method, opts] of Object.entries(methods)) { | ||||||||
if (!opts.id) { | ||||||||
const className = (typeof klass === 'function' ? klass : klass.constructor).name; | ||||||||
opts.id = `lh:${className}:${method}`; | ||||||||
} | ||||||||
/** @type {IsFunction<Class[typeof method]>} */ | ||||||||
const original = klass[method]; | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm concerned that basically everything in this and the above function are typed as I wonder if there's something simpler (or defining more in a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure. My main goal was to only allow property names of functions of |
||||||||
if (!original) { | ||||||||
throw new Error('Cannot decorate non-existent method ${method}'); | ||||||||
} | ||||||||
klass[method] = Log.timeDecorate(original, opts); | ||||||||
} | ||||||||
} | ||||||||
|
||||||||
/** | ||||||||
* @param {string} title | ||||||||
* @param {ArrayAtLeastOne} args | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Was Should we take another step and make it explicit? That would also have the benefit of avoiding the tuple magic. Something like: /**
* @param {string} title
* @param {any} message
* @param {any[]} args
*/
static log(title, message, ...args) {
// ...
} (we could go further and make There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. a quick check also caught a knock on effect in lighthouse/lighthouse-core/lib/console-quieter.js Lines 39 to 41 in bebcd6f
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||||||||
*/ | ||||||||
static log(title, ...args) { | ||||||||
Log.events.issueStatus(title, args); | ||||||||
return Log._logToStdErr(title, args); | ||||||||
Log._logToStdErr(title, args); | ||||||||
} | ||||||||
|
||||||||
/** | ||||||||
* @param {string} title | ||||||||
* @param {ArrayAtLeastOne} args | ||||||||
*/ | ||||||||
static warn(title, ...args) { | ||||||||
Log.events.issueWarning(title, args); | ||||||||
return Log._logToStdErr(`${title}:warn`, args); | ||||||||
Log._logToStdErr(`${title}:warn`, args); | ||||||||
} | ||||||||
|
||||||||
/** | ||||||||
* @param {string} title | ||||||||
* @param {ArrayAtLeastOne} args | ||||||||
*/ | ||||||||
static error(title, ...args) { | ||||||||
return Log._logToStdErr(`${title}:error`, args); | ||||||||
Log._logToStdErr(`${title}:error`, args); | ||||||||
} | ||||||||
|
||||||||
/** | ||||||||
* @param {string} title | ||||||||
* @param {ArrayAtLeastOne} args | ||||||||
*/ | ||||||||
static verbose(title, ...args) { | ||||||||
Log.events.issueStatus(title, args); | ||||||||
return Log._logToStdErr(`${title}:verbose`, args); | ||||||||
Log._logToStdErr(`${title}:verbose`, args); | ||||||||
} | ||||||||
|
||||||||
/** | ||||||||
|
@@ -228,14 +415,24 @@ class Log { | |||||||
static get doubleLightHorizontal() { | ||||||||
return '──'; | ||||||||
} | ||||||||
|
||||||||
/** | ||||||||
* @return {PerformanceEntry[]} | ||||||||
*/ | ||||||||
static takeTimeEntries() { | ||||||||
const entries = marky.getEntries(); | ||||||||
marky.clear(); | ||||||||
return entries; | ||||||||
} | ||||||||
|
||||||||
/** | ||||||||
* @return {PerformanceEntry[]} | ||||||||
*/ | ||||||||
static getTimeEntries() { | ||||||||
return marky.getEntries(); | ||||||||
} | ||||||||
} | ||||||||
|
||||||||
Log.events = new Emitter(); | ||||||||
Log.takeTimeEntries = () => { | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why were these declared here before? I can't remember. Was it important? :) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nope, afaict there was no reason for it There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think there was a good reason. It only blames to me because I split it off from the mega-PR :) Seems fine to me! |
||||||||
const entries = marky.getEntries(); | ||||||||
marky.clear(); | ||||||||
return entries; | ||||||||
}; | ||||||||
Log.getTimeEntries = () => marky.getEntries(); | ||||||||
|
||||||||
module.exports = Log; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is this file used anywhere?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It will be, but I should have removed it as part of breaking up the original PR (#6515). It'll be needed so that importing the logger has types, w/o needing to use the
/** @type
comment everywhere.Will delete from this PR.