-
Notifications
You must be signed in to change notification settings - Fork 477
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
Log Implementation #302
Log Implementation #302
Conversation
Codecov Report
@@ Coverage Diff @@
## develop #302 +/- ##
===========================================
+ Coverage 50.16% 50.84% +0.67%
===========================================
Files 95 97 +2
Lines 3267 3432 +165
Branches 457 477 +20
===========================================
+ Hits 1639 1745 +106
- Misses 1628 1687 +59
Continue to review full report at Codecov.
|
…tter (nothing reads from this), localized strings
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 far as what information to include on each line, my initial thoughts are:
- Log Level
- Timestamp
- Maybe module? I.e. the source file calling
log
.
For metadata that does not change over the course of a session (i.e. version numbers), we only need to log it once per session.
src/shared/logger.ts
Outdated
this.setLevel(level) | ||
} | ||
|
||
public getOutputChannel(): vscode.OutputChannel { |
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.
Better to use property accessors for these:
public get level(): string {
return this.logger.level
}
public set level(value: string): void {
this.logger.level = level
}
src/shared/logger.ts
Outdated
// TODO: Add ability to handle arbitrary values, through meta parameter or LogEntry parameter | ||
export function log(level: string, message: string): void { | ||
logSettings.getLogger().log(level, message) | ||
logSettings.getOutputChannel().appendLine(`[${level}]: ${message}`) |
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 write a winston transport for VS Code output channels instead of doing it manually?
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 would be awesome is winston handled formatting and text color for the OutputChannel.
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'll take a look to see if that's possible.
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 have a rudimentary transport right now, but I'm not 100% sure if the formatted message is accessible (which seems extremely strange to me). Still looking into it though--this would be nice because then we can just return the winston
logger object and to have handlers for all of the different levels and mechanisms.
src/shared/logger.ts
Outdated
// TODO: Format correctly/uniformly | ||
// TODO: Add timestamps | ||
// TODO: Add ability to handle arbitrary values, through meta parameter or LogEntry parameter | ||
export function log(level: string, message: string): 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.
Nit: log(level: 'info' | 'warn' | 'error', ...)
src/shared/logger.ts
Outdated
} | ||
|
||
public setLevel(level: string): void { | ||
// TODO: Ensure log level is valid |
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.
If you change the type from string
to 'info' | 'warn' | 'error'
, the compiler will validate this for us.
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 like this is on an outdated version, current version validates against winston
's set levels: https://github.com/aws/aws-toolkit-vscode/pull/302/files#diff-4fdc113ff641e6c27c6f66c0d90c90b8R40
src/shared/logger.ts
Outdated
|
||
export function initialize(context: vscode.ExtensionContext): void { | ||
const outputChannel: vscode.OutputChannel = vscode.window.createOutputChannel('AWS Toolkit Logs') | ||
const logPath = context.logPath + '.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 context.logPath
a file or directory?
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.
Unclear. context.logPath
doesn't have a file extension, but also doesn't create a directory. I would say that this solution works unless we want to add additional log files
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 validate that context.logPath
does not end with path.sep
, to protect against future changes to the VS Code API implementation?:
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 docs say:
An absolute file path of a directory in which the extension can create log files. The directory might not exist on disk and creation is up to the extension. However, the parent directory is guaranteed to be existent.
src/shared/logger.ts
Outdated
return this.logger | ||
} | ||
|
||
public setLevel(level: string): 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.
Instead of validating the level at runtime, change it's type to 'info' | 'warn' | 'error'
and the compiler will validate it for us.
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.
Could alternately use the winston
preset levels as well.
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.
String enum please :)
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.
- Did you consider supporting
ILogService
to leverage VS Code built-in capabilities? This way we could plug-in to existing framework viaMultiplexLogService
- Consider using VS Code common/log
LogLevel
- How does VS Code do file logging? Can we simply leverage that?
Supporting VS Code's logging integration would enable native logging functionality such:
- Setting the log level
- Show logs (eventually)
- Probably other log access/management that might exist now of in the future
src/shared/logger.ts
Outdated
} | ||
} | ||
|
||
export function initialize(context: vscode.ExtensionContext): 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.
Favor precise, explicit params:
export function initialize(params: {
logPath: Pick<vscode.ExtensionContext, 'logPath'> // or just string
}) : void
src/shared/logger.ts
Outdated
// TODO: Format correctly/uniformly | ||
// TODO: Add timestamps | ||
// TODO: Add ability to handle arbitrary values, through meta parameter or LogEntry parameter | ||
export function log(level: string, message: string): 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.
Favor destructuring params for clarity and support of future API evolution. Argument list is especially dangerous with multiple ordered arguments of the same type because:
- Compile can't help when order is swapped
- Looks reasonable to developer and code reviewer when order is swapped
- Adding multiple optional params later will result in ugly union types and type checking
- Intent of code is unclear to reader unless they open the implementation of the code. This is exacerbated with multiple nested instances of argument list functions/constructors. The only way to understand intent is to read all of the code.
Use should read like this (similar to winston's log):
log({
level: Level.Error ,// consider making level a string enum
message: 'Oh my, something to log'
})
In this case in the future we could do any of these without breaking existing code or doing any crazy union type mumbo jumbo.:
- Add a default for
level
perhaps toLevel.info
- Add an an optional
key
which if provided could look up the message bundle and substitute the message as the first param for the corresponding template - Add an optional boolean
show
attribute that determines whether channel should show when message is logged - Add an optional
preserveFocus?: 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.
This function is removed in my working branch. I refactored to run everything through a winston
transport, which means that we're using purely Winston-based functionality.
src/shared/logger.ts
Outdated
const localize = nls.loadMessageBundle() | ||
let logSettings: LogSettings | ||
|
||
class LogSettings { |
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.
Favor factory pattern to class.
Consider making LogSettings
an interface with a corresponding getLogger(params)
that returns something that perhaps implements logging interface such as ILogService
or an extension of it.
It's worth trying out. All of the private readonly
declarations disappear. No more this.<propertyName> = <propertyName>
. At first a factory might seem like an unexpected approach but in JS/TS it's often a cleaner more flexible solution than a class. This approach is favorable in many languages such as Java where it's often skipped since it's more verbose to implement in Java.
Using a factory getLogger()
could also support the singleton pattern if we decide it's desirable.
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'm not 100% sure if this will work with Winston since I don't want to risk overwriting existing output channels or log files. I can look into it though. See my notes re: ILogService below, however; that is not currently an option.
@micholas I've looked into ILogService...the functionality is not exposed by VSCode and as far as I can tell is not planned to be exposed. It seems like they had grander plans for logging but have since shut them down due to conflicts with the existing output channels. See:
I would prefer to use something internal if it became available as this would mean we can hook into VSCode's log levels and log level selector in the preferences (I'm making a Winston-specific one for the toolkit) but unfortunately at this point it doesn't appear that that's the case. |
Spoke with @micholas and am changing a few things for the next revision: Currently, there are race conditions if
The only reason we need to initialize the logger in
Additionally, looking to refactor this from a class to a function based file. |
…d and modified tests
Realizing that the outputted timestamp is incorrect. Fixing now... |
@@ -303,9 +326,12 @@ | |||
"semver": "^5.6.0", | |||
"sleep-promise": "^8.0.1", | |||
"tcp-port-used": "^1.0.1", | |||
"triple-beam": "^1.3.0", |
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 thought this was removed, but it looks like it made its way back in. Will delete.
src/lambda/commands/invokeLambda.ts
Outdated
console.log('invoking lambda function with the following payload:') | ||
console.log(message.value) | ||
logger.info('invoking lambda function with the following payload:') | ||
logger.info(message.value ? message.value.toString() : '') |
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.
There's a shortcut: Replace message.value ? message.value.toString() : ''
with String(message.value)
.
src/shared/logger.ts
Outdated
let logSettings: LogSettings | ||
// creates default logger object | ||
// can overwrite defaultLogger if you include params | ||
export function initialize(params?: LoggerParams): Logger { |
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 think this is fine as long as each test only calls initialize once, because our mocha tests are executed in serial.
If not, we may need to wrap this and getLogger
with async-lock
.
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.
Does that only work with async functions, or does it force a block of code to run synchronously? There's no actually-written-as-async functionality built into this code (the exception is the registered VS Code open-in-editor function, but that's completely separate from the core logging functionality). There's a quasi-async required if you want to check a file's output, but there's no way to confirm this creation through Winston's functionality, either through async or callback. My workaround for this was the waitForLogFile
function in logger.test.ts
, but this is only required if you need to check the logs output--Winston doesn't care about whether or not the file is created to function.
If you see this as a significant concern, I can move waitForLogFile
to loggerUtils.ts
so we can use this functionality elsewhere.
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.
No action required here, I was just thinking out-loud >_>
src/shared/logger.ts
Outdated
`${padNumber(d.getDate())}${chooseSpacer(isFilename, 'T', ' ')}` + | ||
`${padNumber(d.getHours())}${chooseSpacer(isFilename, '', ':')}` + | ||
`${padNumber(d.getMinutes())}${chooseSpacer(isFilename, '', ':')}` + | ||
`${padNumber(d.getSeconds())}${chooseSpacer(isFilename, '', ':')}` |
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.
Remove this last chooseSpacer
call to remove the trailing colon:
2019-03-01 14:53:12: [ERROR]: InvalidClientTokenId: The security token included in the request is invalid.
src/shared/logger.ts
Outdated
return num < 10 ? '0' + num.toString() : num.toString() | ||
} | ||
|
||
function chooseSpacer(isFilename: boolean, ifTrue: string, ifFalse: string): string { |
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.
Any reason not to just use a ternary statement here? I.e.
isFilename ? '' : '-'
Instead of
chooseSpacer(isFilename, '', '-')
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.
Not really, I think I was just in a bit of a haze of refactoring and thought this would make more sense. Now that I see it written out, it's perfectly readable. I'll go ahead and add that.
src/shared/logger.ts
Outdated
let logSettings: LogSettings | ||
// creates default logger object | ||
// can overwrite defaultLogger if you include params | ||
export function initialize(params?: LoggerParams): Logger { |
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.
No action required here, I was just thinking out-loud >_>
logPath: getDefaultLogPath() | ||
}) | ||
// only the default logger (with default params) gets a registered command | ||
vscode.commands.registerCommand( |
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.
Trying to register a command that is already registered causes an error. It might be worth guarding against this if possible. getCommands might help
src/shared/logger.ts
Outdated
} | ||
|
||
// creates and returns custom logger | ||
// it's the caller's responsibility to keep track of this logger object |
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.
If it is the caller's responsibility to call some sort of dispose/cleanup method, please add what the method(s) are in the comment.
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.
Do we have any conventions for adding method usage instructions to the tooltips? Might be a good place to add that there.
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.
In VS Code if you place your cursor on the (blank) line above a function and type /**
and press enter, you should get the comment template. Within a comment, you can type @
to bring up some of the sections, like description
, param
, and returns
.
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.
src/shared/logger.ts
Outdated
const configuration: SettingsConfiguration = new DefaultSettingsConfiguration(extensionSettingsPrefix) | ||
level = | ||
configuration.readSetting<string>('logLevel') ? | ||
configuration.readSetting<string>('logLevel') as string : DEFAULT_LOG_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.
Consider calling configuration.readSetting<string>('logLevel')
once only
Types of changes
Description
Adding
winston
logging~\AppData\Roaming\Code\logs
~/.config/Code/logs
~Library/Application Support/Code/logs
info
state or higher. This is configurable throughFile > Preferences > Settings > Extensions > AWS Configuration > Log Level
AWS: View AWS Toolkit Logs
for a direct link to the persistent file (vs. the ephemeral Output Channel).Currently logs, look like the following (on Windows):
TBDs
goto log
commands in command palettewinston
can handle, specifically,meta
params andLogEntry
object parameterconsole.*
commands to uselogger.log
Motivation and Context
We don't have logs for users. Now we do!
A bit more context: we only outputted messages to
stdout
andstderr
throughconsole.*
calls. This allows us to set additional logging that persists on disk that can help users troubleshoot and send issues to us.Related Issue(s)
#293
Testing
Tests pass and logs are generated on normal run.
Screenshots (if appropriate)
Checklist
License
By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.