-
Notifications
You must be signed in to change notification settings - Fork 619
Logger & LoggerMiddleware #66
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
Conversation
packages/logger/src/Formatter.ts
Outdated
return res; | ||
} | ||
|
||
private parsePattern(pattern: 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.
This seems like a very heavy solution. Since you know which keys have been provided to you, why not just use String.prototype.replace, e.g., formatString.replace(/{operation}/g, operation)
?
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 parsing function is all based on my assumption that we don't have prior knowledge of keys in the pattern string. Admittedly this is a heavy solution, it is only called in the constructor. You are right, if we have a pre-defined set of keys in the pattern string, the implementation will be much simpler.
packages/logger/src/Formatter.ts
Outdated
this.literalArray = this.parsePattern(pattern); | ||
} | ||
|
||
format(stats: {[key: string]: string|undefined}): 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.
Allowing any key to be supplied means that individual applications will need to develop their own idioms for which keys to supply when calling the logger and which to expect when writing format strings. If we do end up using a structured formatter like this, I would recommend using an interface with known keys.
For example, Ruby defines a fixed set of substitution patterns that will be replaced by the log formatter (http://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/Log/Formatter.html).
packages/logger/src/Formatter.ts
Outdated
|
||
private parsePattern(pattern: string): string[] { | ||
let isExpression: boolean = false; | ||
let res: Array<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.
Please use full word names for all variables other than loop counters.
packages/logger/src/Formatter.ts
Outdated
let tmpStr = ''; | ||
for(let i = 0; i < pattern.length; i++) { | ||
let char = pattern.charAt(i); | ||
if (isExpression && char !== '}') { |
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.
Since keys may be any string, you will need to support escaping control characters; \}
should be interpreted as the string }
and not as the end of an expression.
packages/logger/src/Formatter.ts
Outdated
isExpression = false; | ||
} else if ( | ||
!isExpression && | ||
!(i < pattern.length - 1 && pattern.substring(i, i + 2) === '${') |
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.
Why use a two token sequence to mark the start of expressions? You could use {
instead of ${
, which would make control character escaping simpler. Right now, if a customer wanted to define a log pattern inside of a backtick-quoted string, they would need to escape replacement sequences meant for the log formatter instead of for the JS template string formatter (e.g., User: ${username}; Operation: \${operation}
, where the customer wants to use JS substitution for variables in scope).
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.
Good catch. When design this I just wanted it to look like string template but never thought about that users may use backticks.
return undefined | ||
} | ||
if (shape.sensitive) { | ||
return '***SensitiveInformation***' |
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.
Can we just use ***
?
SerializationModel | ||
} from '@aws/types'; | ||
|
||
export function mapToShape(obj: any, shape: SerializationModel): any { |
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 don't think we should be exposing functions with a return type of any
.
packages/types/src/logger.ts
Outdated
|
||
export type LogLevel = 'info' | 'log' | 'warn' | 'error'; | ||
|
||
export namespace Logging { |
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 package should only contain interfaces and types. Exporting a namespace will create a runtime value.
packages/types/src/logger.ts
Outdated
logLevels?: [LogLevel] | LogLevel | ||
} | ||
|
||
export interface WritableLogger { |
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.
Couldn't this just be called Logger
?
packages/logger/src/index.ts
Outdated
} | ||
|
||
private write(content: string, logLevel: LogLevel) { | ||
if (this.logLevels.indexOf(logLevel) >= 0 && !!this.logger[logLevel]) { |
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 cast isn't necessary here, as the placement of this.logger[logLevel]
as a clause in an if
condition will automatically cause it to be evaluated for truthiness. If you do want an explicit cast, please use Boolean(variable)
instead of !!
.
packages/logger/src/index.ts
Outdated
|
||
private getLogLevels(logLevel: LogLevel): [LogLevel] { | ||
const allLogLevels: [LogLevel] = [LogLevel.Log, LogLevel.Info, LogLevel.Warn, LogLevel.Error]; | ||
return Array.prototype.slice.call(allLogLevels, allLogLevels.indexOf(logLevel), allLogLevels.length); |
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 a huge fan of this approach. Why can't we just use a numeric scale like Log4j, Apache, or Syslog?
constructor( | ||
private readonly next: Handler<any, any>, | ||
private readonly context: HandlerExecutionContext, | ||
private readonly paramsOperation: paramsOperation |
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 will need to be the first constructor argument if you want people to be able to override it. (End users must be able to use this constructor to get to a constructor signature that complies with the middleware interface.) With this constructor, using this class as middleware will cause a compilation error.
packages/types/src/logger.ts
Outdated
info(content: string): void; | ||
} | ||
|
||
export interface paramsOperation { |
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.
Interface names should start with a capital letter, even if the shape they define is a function. The name could also be a bit more descriptive -- there's no indication here what a paramsOperation
function should do.
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 use this name because they are operations used on parameters. The operations don't need to be removing sensitive info, but also can be flattening the structure and so on.
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.
True, but "operation used on parameters" is a generic enough statement that it could be applied to any code.
What's important here is what the operation does to those parameters. I think a better name here would be "SensitiveDataCensor" or "SensitiveDataScrubber" -- something that conveys what the function is expected to do.
packages/types/src/logger.ts
Outdated
@@ -0,0 +1,27 @@ | |||
import {Shape, Member} from './protocol' | |||
|
|||
export enum LogLevel { |
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.
enum
s are objects at runtime and can't be used in the types
package. I think what you want is a string literal union here ('log'|'info'|'warn'|'error'
) and then an enum inside the logger package mapping each of those strings to an integer.
packages/types/src/logger.ts
Outdated
Error = 'error' | ||
} | ||
|
||
export interface LoggerOption { |
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 should be plural (LoggerOptions
).
packages/types/src/logger.ts
Outdated
} | ||
|
||
export interface Logger { | ||
logOperationInfo: 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.
What is this property for?
@@ -0,0 +1,27 @@ | |||
import {Shape, Member} from './protocol' | |||
|
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 add documentation blocks for all interfaces defined.
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.
Overall I think a good first stab. I think your logger is just a little too 'smart', and we need to be more explicit about what the customer needs to provide to it.
packages/types/src/logger.ts
Outdated
} | ||
|
||
export interface LoggerOption { | ||
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.
I think we should define an interface to use here beyond object.
I think we have 2 choices:
- Allow the customer to provide an object with a defined method that accepts input, like
log
orwrite
. - Allow the customer to provide an object with defined methods that roughly correlate to the log level mapping.
Option 1 could look something like:
export interface outputter {
write: (content: string) => void;
}
Option 2 could look something like:
export type outputFn = (content: string) => void;
export interface outputter {
log: outputFn;
info: outputFn;
warn: outputFn;
error: outputFn;
}
Right now with the way the Logger is written, the compiler won't give them any info, and they'll have to find out why their logs don't appear when their application is running (assuming they provide an invalid object).
We could even get real fancy and support both by doing something like prepending the log level to the front on the string if they provided a method that just supported write
!
Right now I'd lean towards option 2 because then customers can do more advanced things like log errors to a different file, and get color coding when using console.
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.
Here I believe using Logger
interface in replacement of object
will be sufficient
packages/types/src/logger.ts
Outdated
Error = 'error' | ||
} | ||
|
||
export interface LoggerOption { |
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.
Future: Might be nice to have options to enable timestamps and log-level logging on anything that's logged:
YYYY-MM-DDTHH:MM:SSZ [level] content
packages/types/src/logger.ts
Outdated
info(content: string): void; | ||
} | ||
|
||
export interface paramsOperation { |
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 know we haven't been great about comments, but can you add jsdocs for the interfaces you are adding? Right now I'm not sure by looking at it what paramsOperation
is used for. Is paramsOperation
the right name to be using as well?
return undefined | ||
} | ||
if (member.sensitive) { | ||
return '******'; |
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: this suggests we use the string <**-redacted-**>
for sensitive fields. I don't know what other SDKs use, but this is what services should return.
return mapObjToShape(obj, member.shape); | ||
} | ||
|
||
export const removeSensitiveLogs: paramsOperation = function (obj: any, member: Member): 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.
As a generic package, I would actually expect this to return an object that's similar to what was passed in as obj
, just with sensitive traits replaced with the redacted
string (or something similar). Returning a stringified JSON seems to specific to the specific problem we're trying to solve. (Plus, if I want to stringify it with new lines and tabs, I'd have to JSON.parse and then JSON.stringify again.)
const {logger} = this.context; | ||
const StartTime = Date.now(); | ||
return this.next.handle(args).then(output => { | ||
if (logger.logOperationInfo) { |
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.
Since this middleware's purpose is to log operation info, I don't think it's necessary to have an extra check in the logger to determine if this info should be logged. The middleware shouldn't care about the logger at all except that it will accept it's content.
If the customer doesn't want this info logged, but does want other info logged, they could always remove this middleware from their stack.
handle(args: HandlerArguments<any>): Promise<any> { | ||
const {input} = args; | ||
const {logger} = this.context; | ||
const StartTime = Date.now(); |
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: only constructors (and interfaces) should start with a capital letter.
* add logger middleware and logger class * update logger level setting
This thread has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs and link to relevant comments in this thread. |
Just the simple logger class that enable users to define a logger level. And a logger middleware to log the operation name and input & output parameters. This middleware won't log anything unless the
logOperationInfo
is set totrue