Skip to content

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

@NatoBoram

Description

@NatoBoram

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions