-
Notifications
You must be signed in to change notification settings - Fork 2
Defer log process. until I/O to avoid expensive op on filtered logs #174
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
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -423,82 +423,74 @@ class RequestLogger { | |
| ); | ||
| return; | ||
| } | ||
| const fields = objectCopy({}, this.fields, logFields || {}); | ||
| const endFlag = isEnd || false; | ||
|
|
||
| /* | ||
| * using Date.now() as it's faster than new Date(). logstash component | ||
| * uses this field to generate ISO 8601 timestamp | ||
| */ | ||
| if (fields.time === undefined) { | ||
| fields.time = Date.now(); | ||
| } | ||
|
|
||
| // eslint-disable-next-line camelcase | ||
| fields.req_id = serializeUids(this.uids); | ||
| if (endFlag) { | ||
| if (this.elapsedTime !== null) { | ||
| // reset elapsedTime to avoid an infinite recursion | ||
| // while logging the error | ||
| this.elapsedTime = null; | ||
| this.error('RequestLogger.end() has been called more than once'); | ||
| } | ||
| this.elapsedTime = process.hrtime(this.startTime); | ||
| // eslint-disable-next-line camelcase | ||
| fields.elapsed_ms = this.elapsedTime[0] * 1000 | ||
| + this.elapsedTime[1] / 1000000; | ||
| } | ||
| const logEntry = { | ||
| level, | ||
| fields, | ||
| msg, | ||
| logFields, | ||
| isEnd: isEnd || false, | ||
| }; | ||
| this.entries.push(logEntry); | ||
|
|
||
| if (LogLevel.shouldLog(level, this.dumpThreshold)) { | ||
| this.entries.forEach(entry => { | ||
| this.doLogIO(entry); | ||
| }); | ||
| this.entries = []; | ||
| this.entries = []; // Flush buffer after dumping | ||
| } else if (LogLevel.shouldLog(level, this.logLevel)) { | ||
| this.doLogIO(logEntry); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * This function transmits a log entry to the configured bunyan logger | ||
| * instance. This way, we can rely on bunyan for actual logging operations, | ||
| * and logging multiplexing, instead of managing that ourselves. | ||
| * This function assembles the final log object from a buffered entry | ||
| * and transmits it to the underlying bunyan logger instance for I/O. | ||
| * | ||
| * @private | ||
| * | ||
| * @param {object} logEntry - The Logging entry to be passed to | ||
| * bunyan | ||
| * @param {string} logEntry.msg - The message to be logged | ||
| * @param {object} logEntry.fields - The data fields to associate to the | ||
| * log entry. | ||
| * | ||
| * @param {object} logEntry - The Logging entry to be processed. | ||
| * @param {string} logEntry.msg - The message to be logged | ||
|
Comment on lines
+451
to
+452
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. should we include other expected fields such as |
||
| * @returns {undefined} | ||
| */ | ||
| doLogIO(logEntry) { | ||
| const fields = objectCopy({}, this.fields, logEntry.logFields || {}); | ||
|
|
||
| if (fields.time === undefined) { | ||
| fields.time = Date.now(); | ||
| } | ||
|
|
||
| // eslint-disable-next-line camelcase | ||
| fields.req_id = serializeUids(this.uids); | ||
| if (logEntry.isEnd) { | ||
| if (this.elapsedTime !== null) { | ||
| // reset elapsedTime to avoid an infinite recursion | ||
| // while logging the error | ||
| this.elapsedTime = null; | ||
| this.error('RequestLogger.end() has been called more than once'); | ||
| } | ||
| this.elapsedTime = process.hrtime(this.startTime); | ||
| // eslint-disable-next-line camelcase | ||
| fields.elapsed_ms = this.elapsedTime[0] * 1000 | ||
| + this.elapsedTime[1] / 1000000; | ||
| } | ||
| switch (logEntry.level) { | ||
| case 'trace': | ||
| this.sLogger.trace(logEntry.fields, logEntry.msg); | ||
| this.sLogger.trace(fields, logEntry.msg); | ||
| break; | ||
| case 'debug': | ||
| this.sLogger.debug(logEntry.fields, logEntry.msg); | ||
| this.sLogger.debug(fields, logEntry.msg); | ||
| break; | ||
| case 'info': | ||
| this.sLogger.info(logEntry.fields, logEntry.msg); | ||
| this.sLogger.info(fields, logEntry.msg); | ||
| break; | ||
| case 'warn': | ||
| this.sLogger.warn(logEntry.fields, logEntry.msg); | ||
| this.sLogger.warn(fields, logEntry.msg); | ||
| break; | ||
| case 'error': | ||
| this.sLogger.error(logEntry.fields, logEntry.msg); | ||
| this.sLogger.error(fields, logEntry.msg); | ||
| break; | ||
| case 'fatal': | ||
| this.sLogger.fatal(logEntry.fields, logEntry.msg); | ||
| this.sLogger.fatal(fields, logEntry.msg); | ||
| break; | ||
| default: | ||
| throw new Error(`Unexpected log level: ${logEntry.level}`); | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.
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.
we need to keep the
objectCopy()here to ensure the fields appearing in the log cannot be affected by code happening after the log call : we need to perform a deep copy before returning...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 that should be up to the caller: I cannot find places where what is logged changes after, this is very uncommon, yet has high CPU impact.
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.
that is the semantics of any logger : you log the value of the object when the call is made. Not respecting this will not break at every call hopefully, but creates a very significant risk that we don't log relevant data....and thus spend even more time debugging than if we did not have log at all.
An alternative would be to
freeze()the object, but this would also cause significant (and breaking!) change on caller, as it may now crash if the variable is modified after being logged. So I don't think it should be used either.Improving performance is good, but correctness and ease/safety of development matter as well : what is the real-life benefit of not copying the object? Is it really worth updating all logging calls and risking to have an issue with every new log line?
Uh oh!
There was an error while loading. Please reload this page.
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.
Thanks for the precision, I understand better as I thought the lack of deep copy was on purpose (we have this bug with the existing code!).
First, on performance, I measured 8% CPU impact on the visible side, and then, it greatly increases the GC pressure. It's hard to precisely measure that part, but it becomes visible when we remove the logs in most places, I can see half of GC duration is from logger. The logger should not be this impactful on the runtime environment... I think we agree on that. Under high load the GC would run up to 50, even 70% of the total duration, clearly a saturation.
On the safety side, I agree. However the original
objectCopywas not a deep copy to begin with, but a shallow copy. We only copy the top level fields to recreate an object. This does not protect against object being edited in between. Here is an example script:Here we log in info, then debug, then edit the object, and finally we log at the dump level: we see that the original object is mutated. Results:
I hope it's easy to read, but in short we can see the mutated object in both the re-printed info log, and the debug log that follows. This is not with the
werelogsof this PR branch, but what we have indevelopment/8.2.So I make two observations:
Popular solutions like winston do not copy at all. They are as such affected by the same issue not yet resolved. So a valid concern indeed, and a real problem inherent with the nodejs language, as they also have a PR, not yet merged, as it has clear performance impacts.
We have this problem since the beginning in our code. But I cannot exclude that in some case, without knowing, we were looking at wrong logs... That's why in my head this was something done on purpose, as we never edit an object we log, or it's very rare. Now if we do not want that, then it's a real bug we want to fix indeed. So for our optimization I do not really see any solution, unless we make it explicit in the lib, that the object is not deep copied, and add an explicit flag if we want it to be deep copied... But not ideal and likely something a developer will miss.
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.
WRLGS-18 was created