Skip to content

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

Merged
merged 14 commits into from
Nov 18, 2017
Merged

Logger & LoggerMiddleware #66

merged 14 commits into from
Nov 18, 2017

Conversation

AllanZhengYP
Copy link
Contributor

@AllanZhengYP AllanZhengYP commented Nov 9, 2017

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 to true

return res;
}

private parsePattern(pattern: 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.

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

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

this.literalArray = this.parsePattern(pattern);
}

format(stats: {[key: string]: string|undefined}): string {
Copy link
Contributor

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


private parsePattern(pattern: string): string[] {
let isExpression: boolean = false;
let res: Array<string> = [];
Copy link
Contributor

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.

let tmpStr = '';
for(let i = 0; i < pattern.length; i++) {
let char = pattern.charAt(i);
if (isExpression && char !== '}') {
Copy link
Contributor

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.

isExpression = false;
} else if (
!isExpression &&
!(i < pattern.length - 1 && pattern.substring(i, i + 2) === '${')
Copy link
Contributor

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

Copy link
Contributor Author

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***'
Copy link
Contributor

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

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.


export type LogLevel = 'info' | 'log' | 'warn' | 'error';

export namespace Logging {
Copy link
Contributor

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.

logLevels?: [LogLevel] | LogLevel
}

export interface WritableLogger {
Copy link
Contributor

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?

@AllanZhengYP AllanZhengYP changed the title [WIP] Logger & Formatter & LoggerMiddleware Logger & LoggerMiddleware Nov 14, 2017
}

private write(content: string, logLevel: LogLevel) {
if (this.logLevels.indexOf(logLevel) >= 0 && !!this.logger[logLevel]) {
Copy link
Contributor

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


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);
Copy link
Contributor

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

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.

info(content: string): void;
}

export interface paramsOperation {
Copy link
Contributor

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.

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

Copy link
Contributor

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.

@@ -0,0 +1,27 @@
import {Shape, Member} from './protocol'

export enum LogLevel {
Copy link
Contributor

Choose a reason for hiding this comment

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

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

Error = 'error'
}

export interface LoggerOption {
Copy link
Contributor

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

}

export interface Logger {
logOperationInfo: boolean;
Copy link
Contributor

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'

Copy link
Contributor

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.

Copy link
Contributor

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

}

export interface LoggerOption {
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.

I think we should define an interface to use here beyond object.

I think we have 2 choices:

  1. Allow the customer to provide an object with a defined method that accepts input, like log or write.
  2. 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.

Copy link
Contributor Author

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

Error = 'error'
}

export interface LoggerOption {
Copy link
Contributor

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

info(content: string): void;
}

export interface paramsOperation {
Copy link
Contributor

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 '******';
Copy link
Contributor

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

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

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

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.

@AllanZhengYP AllanZhengYP dismissed chrisradek’s stale review November 17, 2017 00:14

new commits pushed

@AllanZhengYP AllanZhengYP merged commit 13aebe9 into aws:master Nov 18, 2017
trivikr referenced this pull request in trivikr/aws-sdk-js-v3 Dec 10, 2018
* add logger middleware and logger class

* update logger level setting
@lock
Copy link

lock bot commented Sep 26, 2019

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.

@lock lock bot locked as resolved and limited conversation to collaborators Sep 26, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants