winstonjs/logform

[Bug]: Errors don't get processed correctly by `format.json` / `format.simple`

Opened this issue · 0 comments

The problem

I want to log errors.

const logId = randomUUID()
export const logger = createLogger({
	defaultMeta: { logId },
	format: format.json(),
	level: "debug",
	transports: [new transports.Console({ format: format.simple() })],
})

const err = new Error("This is an error.")
logger.info("logger.info:", { error: err })

This gives me {}.

info: logger.info: {"error":{},"logId":"d9ae9439-4a98-4b4e-ad1a-f94c617cfa30"}

I figured I would transform the error into something loggable:

/** Turns an error into a plain object. */
function parseError(err: Error): ParsedError {
	return JSON.parse(
		JSON.stringify(err, Object.getOwnPropertyNames(err)),
	) as ParsedError
}

/** Handles proper stringification of Buffer and bigint output. */
function replacer(_key: string, value: unknown) {
	if (typeof value === "bigint") return value.toString()
	return value
}

const logId = randomUUID()
export const logger = createLogger({
	defaultMeta: { logId },
	format: format.json({
		replacer: (key, value: unknown) => {
			if (value instanceof Error) return parseError(value)
			return replacer(key, value)
		},
	}),
	level: "debug",
	transports: [new transports.Console({ format: format.simple() })],
})

// Tests
const err = new Error("This is an error.")
logger.info("logger.info:", { error: err })
console.log("console.log:", { error: parseError(err) })

However, the new output is completely detached from the code I've written.

info: logger.info: {"error":{"originalColumn":22,"originalLine":24},"logId":"abe10e0e-0b04-4651-be81-00ede4fca436"}
console.log: {
  error: {
    message: "This is an error.",
    originalLine: 24,
    originalColumn: 22,
    line: 24,
    column: 22,
    sourceURL: "/home/nato/Code/localhost/GitLabAPI/src/logger.ts",
    stack: "Error: This is an error.\n    at module code (/home/nato/Code/localhost/GitLabAPI/src/logger.ts:41:12)"
  }
}

More than half of the fields were completely ignored. If I log inside the format.json lambda, the keys are passed correctly.

What version of Logform presents the issue?

2.6.0

What version of Node are you using?

21.1.0

If this is a TypeScript issue, what version of TypeScript are you using?

5.2.2

If this worked in a previous version of Logform, which was it?

No response

Minimum Working Example

import { randomUUID } from "crypto"
import { createLogger, format, transports } from "winston"

interface ParsedError {
	readonly message: string
	readonly originalLine: number
	readonly originalColumn: number
	readonly line: number
	readonly column: number
	readonly sourceURL: string
	readonly stack: string
}

/** Turns an error into a plain object. */
function parseError(err: Error): ParsedError {
	return JSON.parse(
		JSON.stringify(err, Object.getOwnPropertyNames(err)),
	) as ParsedError
}

/** Handles proper stringification of Buffer and bigint output. */
function replacer(_key: string, value: unknown) {
	if (typeof value === "bigint") return value.toString()
	return value
}

const logId = randomUUID()
export const logger = createLogger({
	defaultMeta: { logId },
	format: format.json({
		replacer: (key, value: unknown) => {
			if (value instanceof Error) return parseError(value)
			return replacer(key, value)
		},
	}),
	level: "debug",
	transports: [new transports.Console({ format: format.simple() })],
})

// Tests
const err = new Error("This is an error.")
logger.info("logger.info:", { error: err })
console.log("console.log:", { error: parseError(err) })

Additional information

If we pre-parse each error individually before passing them to Winston, then it works. The bug only appears when we send an Error instance, which seems like the whole point of using a logger in the first place, so I'm a bit confused at why this doesn't work natively nor after hacking into it.

Btw, I don't see a reason to not export the defaut replacer

logform/json.js

Lines 7 to 18 in ded082a

/*
* function replacer (key, value)
* Handles proper stringification of Buffer and bigint output.
*/
function replacer(key, value) {
// safe-stable-stringify does support BigInt, however, it doesn't wrap the value in quotes.
// Leading to a loss in fidelity if the resulting string is parsed.
// It would also be a breaking change for logform.
if (typeof value === 'bigint')
return value.toString();
return value;
}

🔎 Search Terms

log error, log errors, format.json, format error, format errors