-
Notifications
You must be signed in to change notification settings - Fork 1.2k
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
Support logging exception stack traces when the last argument is Throwable #3960
Conversation
e336792
to
d946148
Compare
private boolean isLastElementThrowable(Object... arguments) { | ||
int length = arguments.length; | ||
return length > 0 && arguments[length - 1] instanceof Throwable; | ||
} |
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.
Checks whether the last argements are of type Throwable
.
private void logWithOptionalThrowable(Level level, String format, Object... arguments) { | ||
if(isLastElementThrowable(arguments)) { | ||
int lastIndex = arguments.length - 1; | ||
Object[] args = Arrays.copyOfRange(arguments, 0, lastIndex); | ||
Throwable t = (Throwable) arguments[lastIndex]; | ||
|
||
logger.log(level, format(format, args), t); | ||
return; | ||
} | ||
|
||
logger.log(level, format(format, arguments)); | ||
} |
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 JdkLogger
, if the last arguments are Throwable
type, stacktrace is also logged.
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'd love to see a test for this.
private boolean isLastElementThrowable(Object... arguments) { | ||
int length = arguments.length; | ||
return length > 0 && arguments[length - 1] instanceof Throwable; | ||
} |
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.
Checks whether the last argements are of type Throwable
.
private synchronized void logWithOptionalThrowable(String level, String format, Object... arguments) { | ||
if(isLastElementThrowable(arguments)) { | ||
int lastIndex = arguments.length - 1; | ||
Object[] args = Arrays.copyOfRange(arguments, 0, lastIndex); | ||
Throwable t = (Throwable) arguments[lastIndex]; | ||
|
||
this.log.format("[%s] (%s) %s\n", level.toUpperCase(), Thread.currentThread().getName(), format(format, args)); | ||
t.printStackTrace(this.log); | ||
return; | ||
} | ||
|
||
this.log.format("[%s] (%s) %s\n", level.toUpperCase(), Thread.currentThread().getName(), format(format, arguments)); | ||
} |
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 ConsoleLogger
, if the last argument is of type Throwable
, it also prints a stack trace.
(only use for TRACE, DEBUG, INFO 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.
Can we have tests to validate this?
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 added test code!
private synchronized void logErrorWithOptionalThrowable(String level, String format, Object... arguments) { | ||
if(isLastElementThrowable(arguments)) { | ||
int lastIndex = arguments.length - 1; | ||
Object[] args = Arrays.copyOfRange(arguments, 0, lastIndex); | ||
Throwable t = (Throwable) arguments[lastIndex]; | ||
|
||
this.err.format("[%s] (%s) %s\n", level.toUpperCase(), Thread.currentThread().getName(), format(format, args)); | ||
t.printStackTrace(this.err); | ||
return; | ||
} | ||
|
||
this.err.format("[%s] (%s) %s\n", level.toUpperCase(), Thread.currentThread().getName(), format(format, arguments)); | ||
} |
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 ConsoleLogger
, if the last arguments are Throwable
type, it also prints a stack trace with the err
PrintStream.
(only use for WARN, ERROR level)
@chemicL |
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.
Thank you for the proposal. I've been occupied with other priority subjects and now found the time to review the PR. I think it is a useful utility. I left some comments, please address them if you'd like to integrate this change. Most importantly, I look forward to tests validating the change has some meaningful effect.
private void logWithOptionalThrowable(Level level, String format, Object... arguments) { | ||
if(isLastElementThrowable(arguments)) { | ||
int lastIndex = arguments.length - 1; | ||
Object[] args = Arrays.copyOfRange(arguments, 0, lastIndex); |
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.
Let's try to avoid copying the array, it isn't necessary.
Object[] args = Arrays.copyOfRange(arguments, 0, lastIndex); | ||
Throwable t = (Throwable) arguments[lastIndex]; | ||
|
||
logger.log(level, format(format, args), t); |
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'd suggest changing the way the format
method works to optionally exclude the last element in case we know it's a Throwable
. Pseudocode:
Throwable t = getPotentialThrowable(arguments);
if (t != null) logger.log(level, format(format, arguments, true), t);
else logger.log(level, format(format, arguments));
The existing format method would then delegate to the one with an optional boolean skipLast
argument with false
.
The getPotentialThrowable
would be similar to isLastElementThrowable
but would return the value or null
.
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 added the skipLast
parameter to the format(...)
method. Since Varargs
must be placed at the end, I added skipLast
as a middle parameter.
Additionally, I overloaded the existing method.
private void logWithOptionalThrowable(Level level, String format, Object... arguments) { | ||
if(isLastElementThrowable(arguments)) { | ||
int lastIndex = arguments.length - 1; | ||
Object[] args = Arrays.copyOfRange(arguments, 0, lastIndex); | ||
Throwable t = (Throwable) arguments[lastIndex]; | ||
|
||
logger.log(level, format(format, args), t); | ||
return; | ||
} | ||
|
||
logger.log(level, format(format, arguments)); | ||
} |
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'd love to see a test for this.
private synchronized void logWithOptionalThrowable(String level, String format, Object... arguments) { | ||
if(isLastElementThrowable(arguments)) { | ||
int lastIndex = arguments.length - 1; | ||
Object[] args = Arrays.copyOfRange(arguments, 0, lastIndex); | ||
Throwable t = (Throwable) arguments[lastIndex]; | ||
|
||
this.log.format("[%s] (%s) %s\n", level.toUpperCase(), Thread.currentThread().getName(), format(format, args)); | ||
t.printStackTrace(this.log); | ||
return; | ||
} | ||
|
||
this.log.format("[%s] (%s) %s\n", level.toUpperCase(), Thread.currentThread().getName(), format(format, arguments)); | ||
} |
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 have tests to validate this?
@@ -507,6 +526,39 @@ final String format(@Nullable String from, @Nullable Object... arguments){ | |||
return null; | |||
} | |||
|
|||
private synchronized void logWithOptionalThrowable(String level, String format, Object... arguments) { |
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.
Same comments as with the JdkLogger.
this.log.format("[%s] (%s) %s\n", level.toUpperCase(), Thread.currentThread().getName(), format(format, arguments)); | ||
} | ||
|
||
private synchronized void logErrorWithOptionalThrowable(String level, String format, Object... arguments) { |
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 method is almost exactly the same as the one above, can you please unify them with a PrintStream
as an argument?
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 merged the methods into one, and modified it to accept a PrintStream
as a parameter.
public synchronized void info(String format, Object... arguments) { | ||
this.log.format("[ INFO] (%s) %s\n", Thread.currentThread().getName(), format(format, arguments)); | ||
public void info(String format, Object... arguments) { | ||
logWithOptionalThrowable("INFO", format, arguments); |
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 am guessing the space was to align INFO under DEBUG and such in a single column so the space was a filler. With the spaces all of (TRACE, DEBUG, INFO, WARN, ERROR) take up the same amount of horizontal space. Please roll back.
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 detailed explanation!
I have reverted the code.
- Enhanced logger methods to check if the last argument is of type Throwable. - If Throwable is detected, its stack trace is included in the log output. - Improves debugging by seamlessly handling exception logging alongside message formatting. ref. https://www.slf4j.org/faq.html#paramException Signed-off-by: raccoonback <[email protected]>
Signed-off-by: raccoonback <[email protected]>
…rgument Signed-off-by: raccoonback <[email protected]>
Signed-off-by: raccoonback <[email protected]>
9df27da
to
4dd4f62
Compare
@chemicL |
if (skipLast) --lastIndex; | ||
for (int index = 0; index < lastIndex; ++index) { | ||
computed = computed.replaceFirst("\\{\\}", Matcher.quoteReplacement(String.valueOf(arguments[index]))); |
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.
Determines whether to replace the last factor depending on skipLast
.
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 follow up. I added a few more comments, please apply them also to similar areas where appropriate. Please check the build results, they are failing with NPE. Before the next round please make sure the build is passing on your machine.
@@ -442,18 +442,45 @@ public void error(String msg, Throwable t) { | |||
} | |||
|
|||
@Nullable | |||
final String format(@Nullable String from, @Nullable Object... arguments){ | |||
if(from != null) { | |||
String format(@Nullable String from, @Nullable Object... arguments) { |
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 one can also be private.
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.
Changed to private
accessor.
for (Object argument : arguments) { | ||
computed = computed.replaceFirst("\\{\\}", Matcher.quoteReplacement(String.valueOf(argument))); | ||
int lastIndex = arguments.length; | ||
if (skipLast) --lastIndex; |
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 make sure you use braces around the instructions following an if condition.
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.
Reflect it!
} | ||
|
||
@Nullable | ||
private String format(@Nullable String from, boolean skipLast, @Nullable Object... arguments) { |
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 added the skipLast parameter to the format(...) method. Since Varargs must be placed at the end, I added skipLast as a middle parameter.
Additionally, I overloaded the existing method.
Would it work if instead of Object...
you used Object[]
? Since this is an internal API it should be totally fine with the usages here. Then the order of arguments will be more appropriate IMO.
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 changed it to use Object[]
instead.
Signed-off-by: raccoonback <[email protected]>
Signed-off-by: raccoonback <[email protected]>
Signed-off-by: raccoonback <[email protected]>
Signed-off-by: raccoonback <[email protected]>
Signed-off-by: raccoonback <[email protected]>
@chemicL Thank you for your detailed review! |
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.
Thank you for the follow up. The production code looks good. Regarding the tests, please refrain from using mocks when more end-to-end tests are possible. The existing tests allow validating the output. Check the use of ConsoleLoggerTest#outContent
and JdkLoggerTest#nullFormatIsAcceptedByUnderlyingLogger
. Also, please revert unrelated formatting changes so that the PR consists only of changes relevant to the problem that's being solved. Thanks.
Signed-off-by: raccoonback <[email protected]>
Signed-off-by: raccoonback <[email protected]>
I have rolled back the unrelated formatting changes. Additionally, since ConsoleLoggerTest already covers all cases for ConsoleLogger, I found the newly added test to be redundant and have therefore removed it. |
I'm not sure I understand. Since the functionality of In Also, make sure the test methods are descriptive as the existing methods are and capture the circumstance and expectation. |
…gger Signed-off-by: raccoonback <[email protected]>
Signed-off-by: raccoonback <[email protected]>
I misunderstood;; |
@raccoonback thank you for the contribution! 🚀 |
Motivation
Hello,
Reactor Core supports SLF4J, Console Logger, and JDK Logger.
In SLF4J implementations (e.g., Logback), when the last argument is of type Throwable, the exception stack trace is automatically logged, as shown below.
(ref. SLF4J FAQ)
(ref: Logback implementation)
However, the Console Logger and JDK Logger supported by Reactor Core do not provide this functionality, and users must manually check for
Throwable
and handle it accordingly in their code.This results in unnecessary branching in user code and decreases consistency across logging mechanisms.
Therefore, in this change, we have modified the
ConsoleLogger
andJdkLogger
to detect if the last argument is of typeThrowable
and automatically log the exception stack trace.Description
This PR enhances the logging functionality in Reactor Core to check if the last argument in logger methods is of type
Throwable
. If aThrowable
is detected, the exception stack trace is included in the log output.Key Changes
Throwable
instances for improved debugging.Example Usage
Before
After
Benefits
Testing