Skip to content

Fix the message in the console log and add the stack trace#354

Merged
w3nl merged 7 commits intov0.5.xfrom
feature/console-stack
Nov 21, 2025
Merged

Fix the message in the console log and add the stack trace#354
w3nl merged 7 commits intov0.5.xfrom
feature/console-stack

Conversation

@w3nl
Copy link
Contributor

@w3nl w3nl commented Nov 21, 2025

Summary by CodeRabbit

Release Notes

  • New Features

    • Enhanced logger configuration with flexible optional parameters and sensible defaults
    • Process-wide error handling for uncaught exceptions and unhandled rejections
  • Improvements

    • Improved structured error logging with automatic error details extraction
    • Enhanced debug mode for better stack trace visibility
    • More robust log formatting and message normalization

✏️ Tip: You can customize this high-level summary in your review settings.

Enhances the console logger to better handle and format errors,
including stack traces and error message extraction.

It also adds global handlers for uncaught exceptions, unhandled
rejections, and process warnings to ensure comprehensive error
logging.
Normalizes error handling within the logger, ensuring errors are properly formatted, and their stacks are available.

Adds logic to convert non-string messages to JSON strings in the console logger preventing errors when logging objects.

Removes debug tap logic that is no longer needed.
Enhances the console logger to handle various message types and formats more effectively.

It now correctly processes errors, including those embedded within other objects, and ensures messages are properly stringified, especially when dealing with non-string or symbolic message types.

Adds stack trace duplication for debug level logging.
Guarantees that warnings are always converted to Error objects
before being passed to the logger. This ensures consistent handling
of warning messages, including their stack traces, in the logging system.
@w3nl w3nl requested a review from Copilot November 21, 2025 10:30
@coderabbitai
Copy link

coderabbitai bot commented Nov 21, 2025

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Walkthrough

The logger module is refactored to support configurable initialization via an optional options object with defaults, introducing level-wrapping for Error handling, process-wide handlers for uncaught exceptions and unhandled rejections, and enhanced console logger formatting with a modular pipeline for improved message extraction and error detail propagation.

Changes

Cohort / File(s) Summary
Logger factory refactoring
src/logger.js
Updated function signature to accept optional options object with defaults; introduced level-wrapping mechanism to intercept Error-based log calls; added one-time process-wide handlers for uncaughtException, unhandledRejection, and warning events; expanded JSDoc documentation.
Test suite
src/logger.test.js
Added comprehensive test coverage for logger creation with console transport configuration, console logger initialization with json/simple formats, Error stack message extraction, and error formatting validation.
Console logger implementation
src/loggers/console.js
Replaced ad-hoc formatter with modular pipeline; introduced helpers for message normalization, Error property handling, and stack trace extraction; added stackHead derivation and conditional stacktrace duplication in debug mode; improved error detail propagation and empty message handling.

Sequence Diagrams

sequenceDiagram
    participant App as Application
    participant Logger as Logger Factory
    participant LevelWrapper as Level Wrapper
    participant Handler as Log Handler
    participant Console as Console Logger
    
    rect rgb(100, 180, 220)
    Note over App,Console: Logger Initialization
    App->>Logger: makeLogger({loggers, level, meta})
    Logger->>Logger: Apply wrapLevel to each level
    Logger-->>App: Return wrapped logger instance
    end
    
    rect rgb(220, 150, 100)
    Note over LevelWrapper,Console: Error Capture Flow
    App->>LevelWrapper: logger.error(new Error("msg"))
    LevelWrapper->>Handler: Convert to structured event<br/>{level, message, error, stack}
    Handler->>Console: Pass structured log
    Console-->>App: Formatted output
    end
    
    rect rgb(150, 200, 100)
    Note over App,Handler: Non-Error Flow
    App->>LevelWrapper: logger.info("message")
    LevelWrapper->>Handler: Pass through unchanged
    Handler->>Console: Format normally
    Console-->>App: Formatted output
    end
Loading
sequenceDiagram
    participant Process as Node.js Process
    participant Logger as Logger Factory
    participant GlobalHandler as Global Handler
    participant Console as Console Logger
    
    rect rgb(200, 100, 100)
    Note over Process,Console: Uncaught Exception Handler
    Process->>Logger: process.on('uncaughtException', ...)
    Logger->>GlobalHandler: Attach one-time handler
    Note over GlobalHandler: Exception occurs
    Process->>GlobalHandler: Emit uncaughtException event
    GlobalHandler->>Console: logger.error(exception)
    Console-->>Process: Formatted error logged
    end
    
    rect rgb(200, 100, 150)
    Note over Process,Console: Unhandled Rejection Handler
    Process->>Logger: process.on('unhandledRejection', ...)
    Logger->>GlobalHandler: Attach one-time handler
    Note over GlobalHandler: Promise rejection occurs
    Process->>GlobalHandler: Emit unhandledRejection event
    GlobalHandler->>Console: logger.error(normalized Error)
    Console-->>Process: Formatted rejection logged
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • src/logger.js: Dense logic with multiple interacting patterns (level-wrapping, process handlers, JSDoc updates); requires careful verification of error normalization and handler attachment semantics
  • src/loggers/console.js: Modular formatter pipeline with multiple new helper functions and conditional logic; requires thorough testing of edge cases in message extraction, stack handling, and debug mode behavior
  • Process-wide handler attachment: Ensure one-time attachment semantics are correctly implemented and error fallback behavior is sound
  • Integration between level wrapper and console formatter: Verify structured event transformation flows correctly through the pipeline

Poem

🐰 The logger hops with new delight,
Errors caught and wrapped up tight,
Processes watched from uncaught heights,
Formatted stacks in debug's light—
A modular feast of logging might! 📝✨

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/console-stack

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

This PR refactors the console logger to improve message extraction and stack trace handling, and adds global process event handlers for uncaught exceptions and unhandled rejections.

  • Refactored message normalization logic into discrete helper functions for better maintainability
  • Added support for extracting messages from Symbol.for('message'), embedded errors, and stack traces
  • Integrated global process event handlers to automatically log uncaught exceptions, unhandled rejections, and warnings

Reviewed Changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 11 comments.

File Description
src/loggers/console.js Refactored formatter with new helper functions to normalize messages and extract them from various sources (symbols, embedded errors, stacks); changed stack trace capture to always be enabled
src/logger.test.js Added basic tests for error message extraction and stack trace derivation in both JSON and simple formatters
src/logger.js Added level wrapping to handle Error instances, integrated global process event handlers for uncaught exceptions/rejections/warnings, removed JSDoc documentation

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +52 to +101
if (!process.__trojsLoggerHandlersAttached) {
process.__trojsLoggerHandlersAttached = true

process.on('uncaughtException', (err) => {
try {
logger.error(err instanceof Error ? err : new Error(String(err)))
} catch {
// eslint-disable-next-line no-console
console.error('UNCAUGHT_EXCEPTION', err)
}
})

process.on('unhandledRejection', (reason) => {
let err
if (reason instanceof Error) {
err = reason
} else if (typeof reason === 'string') {
err = new Error(reason)
} else {
try {
err = new Error(JSON.stringify(reason))
} catch {
err = new Error(String(reason))
}
}
try {
logger.error(err)
} catch {
// eslint-disable-next-line no-console
console.error('UNHANDLED_REJECTION', err)
}
})

process.on('warning', (warning) => {
try {
logger.warn(
warning instanceof Error
? warning
: (
new Error(
`${warning.name}: ${warning.message}\n${warning.stack || ''}`
)
)
)
} catch {
// eslint-disable-next-line no-console
console.warn('PROCESS_WARNING', warning)
}
})
}
Copy link

Copilot AI Nov 21, 2025

Choose a reason for hiding this comment

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

The check-then-set pattern on process.__trojsLoggerHandlersAttached is not atomic. If multiple loggers are created concurrently, there's a potential race condition where multiple handlers could be attached before any of them sets the flag. Consider using a module-level boolean that's set before any async operations, or ensure logger creation is serialized.

Copilot uses AI. Check for mistakes.
Comment on lines +55 to +101
process.on('uncaughtException', (err) => {
try {
logger.error(err instanceof Error ? err : new Error(String(err)))
} catch {
// eslint-disable-next-line no-console
console.error('UNCAUGHT_EXCEPTION', err)
}
})

process.on('unhandledRejection', (reason) => {
let err
if (reason instanceof Error) {
err = reason
} else if (typeof reason === 'string') {
err = new Error(reason)
} else {
try {
err = new Error(JSON.stringify(reason))
} catch {
err = new Error(String(reason))
}
}
try {
logger.error(err)
} catch {
// eslint-disable-next-line no-console
console.error('UNHANDLED_REJECTION', err)
}
})

process.on('warning', (warning) => {
try {
logger.warn(
warning instanceof Error
? warning
: (
new Error(
`${warning.name}: ${warning.message}\n${warning.stack || ''}`
)
)
)
} catch {
// eslint-disable-next-line no-console
console.warn('PROCESS_WARNING', warning)
}
})
}
Copy link

Copilot AI Nov 21, 2025

Choose a reason for hiding this comment

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

The global process event handlers (uncaughtException, unhandledRejection, warning) are registered on every logger creation (when the flag isn't set). This creates a tight coupling between logger instances and process-level error handling. If multiple parts of an application create loggers with different configurations, only the first logger's configuration will be used for these global handlers. Consider documenting this behavior or making it opt-in via configuration.

Copilot uses AI. Check for mistakes.
}

if (embedded) {
info.message = embedded.message || embedded.toString()
Copy link

Copilot AI Nov 21, 2025

Choose a reason for hiding this comment

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

When an embedded error is found, the function unconditionally overwrites info.message even if a valid message already exists. This means that if both info.message and info.error are present, the original message will be lost. The condition should check if info.message is empty or missing before overwriting it, similar to the logic in lines 20-21 of extractSymbolMessage.

Suggested change
info.message = embedded.message || embedded.toString()
if (!info.message || info.message === '') {
info.message = embedded.message || embedded.toString()
}

Copilot uses AI. Check for mistakes.
Comment on lines +36 to +37
const embedded
= (info.error instanceof Error && info.error)
Copy link

Copilot AI Nov 21, 2025

Choose a reason for hiding this comment

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

Syntax error: Missing assignment operator. The line should be:

const embedded =

This will cause a parse error.

Suggested change
const embedded
= (info.error instanceof Error && info.error)
const embedded =
(info.error instanceof Error && info.error)

Copilot uses AI. Check for mistakes.
delete clone.stack
delete clone.error
delete clone.exception
delete clone[SYMBOL_MESSAGE]
Copy link

Copilot AI Nov 21, 2025

Choose a reason for hiding this comment

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

When finalizing an empty message, several properties are deleted from the clone including level, stack, error, exception, and the symbol. However, there's no deletion of stacktrace which is added by duplicateStackTraceIfDebug. If stacktrace is present in the info object, it might be included in the JSON stringification, which may not be intended.

Suggested change
delete clone[SYMBOL_MESSAGE]
delete clone[SYMBOL_MESSAGE]
delete clone.stacktrace

Copilot uses AI. Check for mistakes.

const simpleFormatter = winston.format.combine(
ensureErrorProps(),
winston.format.errors({ stack: true }),
Copy link

Copilot AI Nov 21, 2025

Choose a reason for hiding this comment

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

The winston.format.errors({ stack: true }) now always captures stack traces regardless of the logger?.debug setting. Previously, stack trace capture was conditional on logger?.debug ?? false. This means stack traces will now be captured even when debug mode is off, which could have performance implications and may expose sensitive information in production logs. The display of stack traces in the output is still controlled by the debug flag at line 119, but the capture happens unconditionally.

Copilot uses AI. Check for mistakes.
Comment on lines +60 to +93
const stringifyNonStringMessage = (info) => {
if (info.message && typeof info.message !== 'string') {
try {
info.message = JSON.stringify(info.message)
} catch {
info.message = String(info.message)
}
return info
})(),
simpleLoggerWithStack
}
}

const deriveMessageFromStack = (info) => {
if (info.stack && (!info.message || info.message === '')) {
info.message = stackHead(info.stack)
}
}

const finalizeEmptyMessage = (info) => {
if (!info.message || info.message === '') {
const clone = { ...info }
delete clone.level
delete clone.stack
delete clone.error
delete clone.exception
delete clone[SYMBOL_MESSAGE]
const keys = Object.keys(clone)
info.message = keys.length > 0 ? JSON.stringify(clone) : ''
}
}

const duplicateStackTraceIfDebug = (info) => {
if ((logger?.debug ?? false) && info.stack) {
info.stacktrace = info.stack
}
}
Copy link

Copilot AI Nov 21, 2025

Choose a reason for hiding this comment

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

The helper functions extractSymbolMessage, attachEmbeddedError, stringifyNonStringMessage, deriveMessageFromStack, finalizeEmptyMessage, and duplicateStackTraceIfDebug do not return values but modify the info object in-place. For consistency with Winston format functions (which return the info object), these should return info or be clearly documented as side-effect functions.

Copilot uses AI. Check for mistakes.
Comment on lines +52 to +53
if (!process.__trojsLoggerHandlersAttached) {
process.__trojsLoggerHandlersAttached = true
Copy link

Copilot AI Nov 21, 2025

Choose a reason for hiding this comment

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

Using process.__trojsLoggerHandlersAttached pollutes the process object with a custom property. This is not a recommended practice as it could conflict with other code or future Node.js features. Consider using a module-level variable or WeakMap to track whether handlers have been attached.

Copilot uses AI. Check for mistakes.

const jsonFormatter = winston.format.combine(
ensureErrorProps(),
winston.format.errors({ stack: true }),
Copy link

Copilot AI Nov 21, 2025

Choose a reason for hiding this comment

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

The winston.format.errors({ stack: true }) now always captures stack traces regardless of the logger?.debug setting. Previously, stack trace capture was conditional on logger?.debug ?? false. This means stack traces will now be captured even when debug mode is off, which could have performance implications and may expose sensitive information in production logs. The duplicateStackTraceIfDebug function only controls whether stacktrace property is added, not whether the stack is captured initially.

Copilot uses AI. Check for mistakes.
w3nl and others added 2 commits November 21, 2025 11:40
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Configures the visibility of stack traces in the console logger based on the debug level.

It disables stack traces unless the logger's debug level is enabled, reducing verbosity in production environments.
@w3nl w3nl requested a review from Copilot November 21, 2025 10:45
@w3nl
Copy link
Contributor Author

w3nl commented Nov 21, 2025

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Nov 21, 2025

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@w3nl
Copy link
Contributor Author

w3nl commented Nov 21, 2025

@coderabbitai summary

@coderabbitai
Copy link

coderabbitai bot commented Nov 21, 2025

✅ Actions performed

Summary regeneration triggered.

Refines JSDoc comments to improve clarity and accuracy
regarding the logger configuration options.

The changes clarify the types of the `options` and
`options.loggers` parameters, replacing generic `Object`
types with more specific descriptions.
@w3nl w3nl merged commit dd5fd8d into v0.5.x Nov 21, 2025
10 of 12 checks passed
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 6 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

* @param {Array<{[key: string]: string}>} [options.loggers=defaultLoggers] - Array of logger transport configurations.
* @param {string} [options.level='info'] - Minimum log level for the logger.
* @param {object} [options.meta={}] - Default metadata to include in all log messages.
* @returns {LoggerType} Winston logger instance with custom level wrappers.
Copy link

Copilot AI Nov 21, 2025

Choose a reason for hiding this comment

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

The @returns type annotation is incorrect. The function returns a Winston logger instance (winston.Logger), not LoggerType which is the configuration schema type imported from './models/schemas/logger.js'.

Change to: @returns {winston.Logger} Winston logger instance with custom level wrappers.

Suggested change
* @returns {LoggerType} Winston logger instance with custom level wrappers.
* @returns {winston.Logger} Winston logger instance with custom level wrappers.

Copilot uses AI. Check for mistakes.
Comment on lines +70 to +71
try {
logger.error(err instanceof Error ? err : new Error(String(err)))
Copy link

Copilot AI Nov 21, 2025

Choose a reason for hiding this comment

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

Using process.__trojsLoggerHandlersAttached as a flag to prevent duplicate event handlers has a race condition. If multiple logger instances are created concurrently (e.g., during module initialization), they could both check the flag before either sets it, leading to duplicate event handlers being attached.

Consider using a more robust approach like checking if handlers are already attached by inspecting process.listenerCount() or using a proper synchronization mechanism.

Copilot uses AI. Check for mistakes.
process.__trojsLoggerHandlersAttached = true

process.on('uncaughtException', (err) => {
try {
Copy link

Copilot AI Nov 21, 2025

Choose a reason for hiding this comment

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

Modifying the process object with custom properties like __trojsLoggerHandlersAttached is not a recommended practice as it pollutes the global namespace and could conflict with other libraries or code using the same pattern.

Consider using a module-level variable instead:

let handlersAttached = false

// Later in code:
if (!handlersAttached) {
  handlersAttached = true
  // attach handlers...
}

Copilot uses AI. Check for mistakes.
return stack ? `${text}\n${stack}` : text
})
if (info.message instanceof Error) {
info.message = info.message.message || info.message.toString()
Copy link

Copilot AI Nov 21, 2025

Choose a reason for hiding this comment

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

The function returns early when info.message instanceof Error, but this happens after checking if (info instanceof Error). However, if info instanceof Error is true and info.message is also an Error, the code on line 49 will execute (extracting the message), but then the function returns without setting the stack. This could lead to missing stack traces for nested errors.

Consider preserving the stack from info.message if it exists:

if (info.message instanceof Error) {
  const msgError = info.message
  info.message = msgError.message || msgError.toString()
  if (!info.stack && msgError.stack) {
    info.stack = msgError.stack
  }
  return
}
Suggested change
info.message = info.message.message || info.message.toString()
const msgError = info.message
info.message = msgError.message || msgError.toString()
if (!info.stack && msgError.stack) {
info.stack = msgError.stack
}

Copilot uses AI. Check for mistakes.
Comment on lines +22 to +27
* @param {object} [options={}] - Logger configuration options.
* @param {Array<{[key: string]: string}>} [options.loggers=defaultLoggers] - Array of logger transport configurations.
* @param {string} [options.level='info'] - Minimum log level for the logger.
* @param {object} [options.meta={}] - Default metadata to include in all log messages.
* @returns {LoggerType} Winston logger instance with custom level wrappers.
* These handlers will log errors and warnings using the logger, and are only attached once per process.
Copy link

Copilot AI Nov 21, 2025

Choose a reason for hiding this comment

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

@sideeffect is not a standard JSDoc tag. Consider using @sideEffect (with capital E) or standard JSDoc conventions like documenting this behavior in the function description or using @fires for events.

Suggested change
* @param {object} [options={}] - Logger configuration options.
* @param {Array<{[key: string]: string}>} [options.loggers=defaultLoggers] - Array of logger transport configurations.
* @param {string} [options.level='info'] - Minimum log level for the logger.
* @param {object} [options.meta={}] - Default metadata to include in all log messages.
* @returns {LoggerType} Winston logger instance with custom level wrappers.
* These handlers will log errors and warnings using the logger, and are only attached once per process.
*
* Side effects: This function attaches process-level event handlers for 'uncaughtException', 'unhandledRejection', and 'warning'.
* These handlers will log errors and warnings using the logger, and are only attached once per process.
*
* @fires process#uncaughtException
* @fires process#unhandledRejection
* @fires process#warning
* @param {object} [options={}] - Logger configuration options.
* @param {Array<{[key: string]: string}>} [options.loggers=defaultLoggers] - Array of logger transport configurations.
* @param {string} [options.level='info'] - Minimum log level for the logger.
* @param {object} [options.meta={}] - Default metadata to include in all log messages.
* @returns {LoggerType} Winston logger instance with custom level wrappers.

Copilot uses AI. Check for mistakes.
Comment on lines +103 to +111
? warning
: (
new Error(
`${warning.name}: ${warning.message}\n${warning.stack || ''}`
)
)
)
} catch {
// eslint-disable-next-line no-console
Copy link

Copilot AI Nov 21, 2025

Choose a reason for hiding this comment

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

The 'warning' event handler creates a custom error message by concatenating warning.name, warning.message, and warning.stack, but this assumes warning is a Node.js Warning object with these properties. However, the conditional on line 106 checks if warning instanceof Error, suggesting warnings might not always have these properties.

Consider handling cases where warning.name or warning.message might be undefined:

const message = warning.name && warning.message 
  ? `${warning.name}: ${warning.message}\n${warning.stack || ''}` 
  : String(warning)
new Error(message)

Copilot uses AI. Check for mistakes.
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.

2 participants