-
Notifications
You must be signed in to change notification settings - Fork 1.1k
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
fix: clean up example-log-extension #952
Conversation
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.
Looks great! Just one comment below.
|
||
export class LogComponent implements Component { | ||
providers?: ProviderMap = { | ||
[EXAMPLE_LOG_BINDINGS.TIMER]: TimerProvider, | ||
[EXAMPLE_LOG_BINDINGS.LOG_ACTION]: LogActionProvider, | ||
[EXAMPLE_LOG_BINDINGS.APP_LOG_LEVEL]: LogLevelProvider, |
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.
The purpose of having a LogLevelProvider
was to provide a default log
level when the component is bound. Should this not be the case anymore? If so, it should be updated in the readme.
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.
As I mentioned in the PR description, the log level should be set by the app. Having a provider for the default value is overkill. There are two perspectives:
- The LogAction has a optional (to be added by Improve dependency injection/resolution #946) dependency of
level
. The default isWARN
. - From the component's view, forcing it to contribute a provider for a single default value is not ideal. That's why I'm proposing to allow components to expose bindings of any flavor in [WIP] feat(context): Allow components to expose a list of bindings #929.
I'll update the README.
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.
I like the idea of changing the log level into an optional dependency that has the default value configured here in the component, and getting rid of LogLevelProvider along the way 👍
7cbaea6
to
b89dd96
Compare
This isn't a blocker but just wondering ... |
We can comment the code instructing istanbul to ignore this block. There is no point to artifically boost the coverage number. |
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.
IIUC, the test suite is no longer verifying the behaviour of the default console logger. It would be good to have some integration or acceptance test using Sinon to intercept console.*
calls and verify the implementation of the console logger too. However, it looks like this was not part of the original code, therefore it's most likely out of scope of your pull request.
I have few more comments, see below.
constructor( | ||
@inject.getter(CoreBindings.CONTROLLER_CLASS) | ||
private readonly getController: Getter<Constructor<{}>>, | ||
@inject.getter(CoreBindings.CONTROLLER_METHOD_NAME) | ||
private readonly getMethod: Getter<string>, | ||
@inject(EXAMPLE_LOG_BINDINGS.APP_LOG_LEVEL) | ||
private readonly logLevel: number, | ||
// Use a getter to defer to resolution of Logger | ||
@inject.getter(EXAMPLE_LOG_BINDINGS.LOGGER) public getLogger: Getter<LogFn>, |
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.
Please use property-based injection for dependencies that have reasonable default. That way users instantiating this Provider class directly (typically unit tests) don't have to repeat specification of default argument values (i.e. logToConsole).
See our own docs: http://loopback.io/doc/en/lb4/Dependency-injection.html#property-injection (emphasis is mine)
Property injection is usually used for optional dependencies which are not required for the class to function or for dependencies that have a reasonable default.
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.
@raymondfeng I still think we should be using property-based injection here, it's the best practice we are recommending ourselves in our docs! If you disagree then please explain your reasoning.
let logger: LogFn; | ||
try { | ||
// Try to see if an external logger is bound | ||
// This is a workaround before optional injection is supported |
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.
This may not be necessary if property-based injection (see my comment above) already supports optional injection.
If it does not, then please add a link to the issue or pull request tracking the task of adding support for optional injection.
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.
@@ -23,4 +23,6 @@ export type LevelMetadata = {level: number}; | |||
|
|||
export type HighResTime = [number, number]; // [seconds, nanoseconds] | |||
|
|||
export type LogFn = (msg: string, level: number) => void; |
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.
I am confused about the difference between LogFn
and LogAction
. Please add tsdoc comments to clarify.
|
||
export class LogComponent implements Component { | ||
providers?: ProviderMap = { | ||
[EXAMPLE_LOG_BINDINGS.TIMER]: TimerProvider, | ||
[EXAMPLE_LOG_BINDINGS.LOG_ACTION]: LogActionProvider, | ||
[EXAMPLE_LOG_BINDINGS.APP_LOG_LEVEL]: LogLevelProvider, |
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.
I like the idea of changing the log level into an optional dependency that has the default value configured here in the component, and getting rid of LogLevelProvider along the way 👍
I don't consider tests verifying the console logger as artificial boosting of our coverage numbers, see my comment above.
In that light, I think adding tests for the console logger should be a part of this pull request. |
@raymondfeng Thank you for opening this pull request, I really appreciate that you are cleaning up the outdated log-extension example and bringing it up to date with our latest APIs & best practices ❤️ |
e194b06
to
2ff495b
Compare
@bajtos PTAL. |
constructor( | ||
@inject.getter(CoreBindings.CONTROLLER_CLASS) | ||
private readonly getController: Getter<Constructor<{}>>, | ||
@inject.getter(CoreBindings.CONTROLLER_METHOD_NAME) | ||
private readonly getMethod: Getter<string>, | ||
@inject(EXAMPLE_LOG_BINDINGS.APP_LOG_LEVEL) | ||
private readonly logLevel: number, | ||
// Use a getter to defer to resolution of Logger | ||
@inject.getter(EXAMPLE_LOG_BINDINGS.LOGGER) public getLogger: Getter<LogFn>, |
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.
@raymondfeng I still think we should be using property-based injection here, it's the best practice we are recommending ourselves in our docs! If you disagree then please explain your reasoning.
@@ -7,6 +7,9 @@ | |||
|
|||
import {ParsedRequest, OperationArgs} from '@loopback/rest'; | |||
|
|||
/** | |||
* A function to perform REST req/res logging action | |||
*/ | |||
export interface LogFn { |
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.
This is out of sync with README. README uses LogAction
, the source code here uses LogFn
. Please pick one name and use it consistently everywhere.
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.
Fixed
a86b2a3
to
8a54686
Compare
8a54686
to
d062a5d
Compare
@bajtos PTAL |
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.
Almost there!
}); | ||
}); | ||
|
||
async function getLogger(inMemory: boolean) { |
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.
Boolean arguments are an anti-pattern, the code getLogger(false)
and getLogger(true)
is difficult to understand.
I am proposing to change inMemory: boolean
to logWriter?: LogWriterFn
.
Usage:
// log to console
getLogger();
// log to memory
getLogger(logToMemory);
private logWriter: LogWriterFn = logToConsole; | ||
|
||
@inject(EXAMPLE_LOG_BINDINGS.APP_LOG_LEVEL, {optional: true}) | ||
private logLevel: number = LOG_LEVEL.WARN; |
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.
Both logWriter
and logLevel
must be public, so that consumers of this class can customize them explicitly (without depending on Dependency Injection).
function timer(startTime?: HighResTime): HighResTime { | ||
if (!startTime) return [3, 3]; | ||
else return [0, 100000002]; | ||
const context: Context = new Context(); |
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.
Unit tests should not be using Context, they should pass arguments directly.
const provider = new LogActionProvider(TestClass, 'test', timer);
if (logWriter) provider.logWriter = logWriter;
return provider.value();
This approach has an important advantage - when the dependencies of the log action provider change, the TypeScript compiler will remind us to update this test. When using context to resolve dependencies, we won't discover the problem until runtime.
constructor( | ||
@inject.getter(CoreBindings.CONTROLLER_CLASS) | ||
private readonly getController: Getter<Constructor<{}>>, | ||
@inject.getter(CoreBindings.CONTROLLER_METHOD_NAME) | ||
private readonly getMethod: Getter<string>, | ||
@inject(EXAMPLE_LOG_BINDINGS.APP_LOG_LEVEL) | ||
private readonly logLevel: number, | ||
@inject(EXAMPLE_LOG_BINDINGS.TIMER) public timer: TimerFn, |
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.
While we are cleaning up the dependencies - I think timer
should be an optional dependency injected via a property, defaulting to the hrtime-based implementation. Thoughts?
(It's ok to leave this change out of scope of this pull request.)
@virkt25 would you like to take another look too? |
c36c6f8
to
420111c
Compare
@bajtos I fixed the unit test and leave |
@bajtos PTAL. |
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.
LGTM 👍
I have few comments to consider. No further review from me is needed even if you decide to address them.
console.log(chalk.red(`ERROR: ${log}`)); | ||
break; | ||
} | ||
this.logWriter(msg, level); |
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.
Nitpick (feel free to ignore): this.write
or this.writeLog
would read more naturally (method names should start with a verb).
EXAMPLE_LOG_BINDINGS.METADATA, | ||
controllerClass.prototype, | ||
methodName, | ||
) || {level: LOG_LEVEL.OFF} |
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.
Would it make sense to extend getMethodMetadata
with an additional argument allowing callers to specify the default value? (It's out of scope of this pull request, just asking.)
It can be useful especially when falsey metadata values are allowed, e.g. a number 0
.
timer, | ||
); | ||
|
||
provider.logLevel = LOG_LEVEL.WARN; |
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.
Isn't this line redundant, considering that logLevel
is set to WARN
by default?
@raymondfeng I've just landed #975, so this PR probably needs to be rebased again (sorry) |
This PR is a follow-up to the following issue based on review comments: #939 - Demonstrate how to plug in a different log (console vs memory) - Switch to @loopback/metadata for decorator implementation - Remove log level provider as it's supposed to be contributed by the app The exercise also exposes a few needs: 1. Support for optional dependency so that we can fall back a default implementation (or a better way) 2. Allow a component to bind artifacts beyond providers
420111c
to
270c83a
Compare
This PR is a follow-up to the following issue based on review comments:
#939
The exercise also exposes a few needs:
implementation (or a better way) (See Improve dependency injection/resolution #946)
See also #935
Checklist
npm test
passes on your machinepackages/cli
were updatedpackages/example-*
were updated