Skip to content
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

Merged
merged 12 commits into from
Mar 7, 2019
Merged

Log Implementation #302

merged 12 commits into from
Mar 7, 2019

Conversation

bryceitoc9
Copy link
Contributor

@bryceitoc9 bryceitoc9 commented Feb 20, 2019

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Infrastructure (change which improves the lifecycle management (CI/CD, build, package, deploy, lint, etc) of the application, but does not change functionality.)
  • Technical debt (change which improves the maintainability of the codebase, but does not change functionality)
  • Testing (change which modifies or adds test coverage, but does not change functionality)
  • Documentation (change which modifies or adds documentation, but does not change functionality)

Description

Adding winston logging

  • Logs are outputted to both an in-IDE output channel and to the path located at:
    • Windows: ~\AppData\Roaming\Code\logs
    • Linux: ~/.config/Code/logs
    • Mac: ~Library/Application Support/Code/logs
  • Logs by default capture anything at an info state or higher. This is configurable through File > Preferences > Settings > Extensions > AWS Configuration > Log Level
  • Same log file/output channel is usable across files by default
    • Additional loggers can also be created albeit without a "global" scope (you'll have to keep track of the object yourself)
  • Default log file can be opened via Command Palette: 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):

Error logs for this session can be found in <user directory>\AppData\Roaming\Code\logs\aws_toolkit\aws_toolkit_<timestamp>.log
2019-03-01 14:27:12: [INFO]: > Downloading latest toolkits endpoint data

TBDs

  • Check if log level is valid before setting value
  • Register goto log commands in command palette
  • Register setting for log level
    • x ] Use value set in settings menu as log level
  • Develop comprehensive formatting rules for log entries
    • Do we want to be uniform across output console and log file? --> yes
    • Add timestamps
    • Handle arbitrary values that winston can handle, specifically, meta params and LogEntry object parameter
  • Backport all existing console.* commands to use logger.log

Motivation and Context

We don't have logs for users. Now we do!

A bit more context: we only outputted messages to stdout and stderr through console.* 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

  • I have read the README document
  • I have read the CONTRIBUTING document
  • My code follows the code style of this project
  • I have added tests to cover my changes
  • All new and existing tests passed
  • A short description of the change has been added to the CHANGELOG

License

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.

@codecov-io
Copy link

codecov-io commented Feb 20, 2019

Codecov Report

Merging #302 into develop will increase coverage by 0.67%.
The diff coverage is 55.44%.

Impacted file tree graph

@@             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
Impacted Files Coverage Δ
src/shared/sam/cli/samCliBuild.ts 0% <0%> (ø) ⬆️
src/shared/defaultResourceFetcher.ts 0% <0%> (ø) ⬆️
src/extension.ts 0% <0%> (ø) ⬆️
src/shared/codelens/typescriptCodeLensProvider.ts 0% <0%> (ø) ⬆️
src/shared/credentials/userCredentialsUtils.ts 88.63% <100%> (+0.54%) ⬆️
src/lambda/lambdaPolicy.ts 98.64% <100%> (+0.05%) ⬆️
src/shared/treeview/awsTreeErrorHandlerNode.ts 100% <100%> (ø) ⬆️
src/shared/sam/cli/samCliDeploy.ts 14.28% <16.66%> (+1.78%) ⬆️
src/shared/sam/cli/samCliPackage.ts 13.63% <20%> (+2.52%) ⬆️
src/shared/sam/cli/samCliInit.ts 14.28% <20%> (+2.52%) ⬆️
... and 13 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update d366cc9...0dc2134. Read the comment docs.

…tter (nothing reads from this), localized strings
Copy link
Contributor

@mpiroc mpiroc left a 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.

this.setLevel(level)
}

public getOutputChannel(): vscode.OutputChannel {
Copy link
Contributor

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
}

// 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}`)
Copy link
Contributor

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?

Copy link
Contributor

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.

Copy link
Contributor Author

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.

Copy link
Contributor Author

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.

// 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 {
Copy link
Contributor

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', ...)

}

public setLevel(level: string): void {
// TODO: Ensure log level is valid
Copy link
Contributor

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.

Copy link
Contributor Author

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


export function initialize(context: vscode.ExtensionContext): void {
const outputChannel: vscode.OutputChannel = vscode.window.createOutputChannel('AWS Toolkit Logs')
const logPath = context.logPath + '.log'
Copy link
Contributor

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?

Copy link
Contributor Author

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

Copy link
Contributor

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?:

Copy link
Contributor

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.

return this.logger
}

public setLevel(level: string): void {
Copy link
Contributor

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.

Copy link
Contributor Author

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.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

String enum please :)

Copy link
Contributor

@micholas micholas left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. Did you consider supporting ILogService to leverage VS Code built-in capabilities? This way we could plug-in to existing framework via MultiplexLogService
  2. Consider using VS Code common/log LogLevel
  3. How does VS Code do file logging? Can we simply leverage that?

Supporting VS Code's logging integration would enable native logging functionality such:

}
}

export function initialize(context: vscode.ExtensionContext): void {
Copy link
Contributor

@micholas micholas Feb 21, 2019

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

// 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 {
Copy link
Contributor

@micholas micholas Feb 21, 2019

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:

  1. Compile can't help when order is swapped
  2. Looks reasonable to developer and code reviewer when order is swapped
  3. Adding multiple optional params later will result in ugly union types and type checking
  4. 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.:

  1. Add a default for level perhaps to Level.info
  2. 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
  3. Add an optional boolean show attribute that determines whether channel should show when message is logged
  4. Add an optional preserveFocus?: boolean

Copy link
Contributor Author

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.

const localize = nls.loadMessageBundle()
let logSettings: LogSettings

class LogSettings {
Copy link
Contributor

@micholas micholas Feb 21, 2019

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.

Copy link
Contributor Author

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.

@bryceitoc9
Copy link
Contributor Author

bryceitoc9 commented Feb 22, 2019

@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.

@bryceitoc9
Copy link
Contributor Author

Spoke with @micholas and am changing a few things for the next revision:

Currently, there are race conditions if getLogger is called before initializing the logger object in extension.ts. This shows up in two places:

  • Anywhere that getLogger is called outside of a function or a class since this will be picked up when loading dependencies.
  • While running tests since tests do not initiate the whole extension.

The only reason we need to initialize the logger in extension.ts is because we currently rely on the vscode.ExtensionContext object that this receives for the file path. We have decided that the best way around this will be to use a general AWS log directory; this will allow us to create logs no matter what by initializing the logger internally to the logger file if it doesn't already exist.

  • Windows stores logs in ~\AppData\Roaming\Code\logs
  • Linux stores logs in ~/.config/Code/logs
  • Mac stores logs in ~Library/Application Support/Code/logs

Additionally, looking to refactor this from a class to a function based file.

@bryceitoc9 bryceitoc9 requested a review from awschristou March 1, 2019 22:08
@bryceitoc9 bryceitoc9 changed the title Initial winston logging implementation (DO NOT SHIP YET--MANY TBDS) Initial winston logging implementation Mar 1, 2019
@bryceitoc9
Copy link
Contributor Author

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",
Copy link
Contributor Author

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.

@bryceitoc9 bryceitoc9 changed the title Initial winston logging implementation Log Implementation Mar 1, 2019
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() : '')
Copy link
Contributor

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).

let logSettings: LogSettings
// creates default logger object
// can overwrite defaultLogger if you include params
export function initialize(params?: LoggerParams): Logger {
Copy link
Contributor

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.

Copy link
Contributor Author

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.

Copy link
Contributor

@mpiroc mpiroc Mar 4, 2019

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 >_>

`${padNumber(d.getDate())}${chooseSpacer(isFilename, 'T', ' ')}` +
`${padNumber(d.getHours())}${chooseSpacer(isFilename, '', ':')}` +
`${padNumber(d.getMinutes())}${chooseSpacer(isFilename, '', ':')}` +
`${padNumber(d.getSeconds())}${chooseSpacer(isFilename, '', ':')}`
Copy link
Contributor Author

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.

return num < 10 ? '0' + num.toString() : num.toString()
}

function chooseSpacer(isFilename: boolean, ifTrue: string, ifFalse: string): string {
Copy link
Contributor

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, '', '-')

Copy link
Contributor Author

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.

let logSettings: LogSettings
// creates default logger object
// can overwrite defaultLogger if you include params
export function initialize(params?: LoggerParams): Logger {
Copy link
Contributor

@mpiroc mpiroc Mar 4, 2019

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(
Copy link
Contributor

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

}

// creates and returns custom logger
// it's the caller's responsibility to keep track of this logger object
Copy link
Contributor

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.

Copy link
Contributor Author

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.

Copy link
Contributor

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.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

const configuration: SettingsConfiguration = new DefaultSettingsConfiguration(extensionSettingsPrefix)
level =
configuration.readSetting<string>('logLevel') ?
configuration.readSetting<string>('logLevel') as string : DEFAULT_LOG_LEVEL
Copy link
Contributor

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

@bryceitoc9 bryceitoc9 merged commit 79963f8 into develop Mar 7, 2019
@bryceitoc9 bryceitoc9 deleted the bryceito/winston-logging branch March 7, 2019 00:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants