Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 31 additions & 3 deletions packages/common/services/console-logger.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,12 @@ export interface ConsoleLoggerOptions {
* The context of the logger.
*/
context?: string;
/**
* If enabled, will force the use of console.log/console.error instead of process.stdout/stderr.write.
* This is useful for test environments like Jest that can buffer console calls.
* @default false
*/
forceConsole?: boolean;
Copy link

@yawhide yawhide Aug 5, 2025

Choose a reason for hiding this comment

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

if i dont ever instantiate this class, i won't be able to set this to true right?

can we add a config so that when i do NestFactory.create(...), i can set the forceConsole to true?

/**
* If enabled, will print the log message in a single line, even if it is an object with multiple properties.
* If set to a number, the most n inner elements are united on a single line as long as all properties fit into breakLength. Short array elements are also grouped together.
Expand Down Expand Up @@ -334,7 +340,15 @@ export class ConsoleLogger implements LoggerService {
timestampDiff,
);

process[writeStreamType ?? 'stdout'].write(formattedMessage);
if (this.options.forceConsole) {
if (writeStreamType === 'stderr') {
console.error(formattedMessage.trim());
} else {
console.log(formattedMessage.trim());
}
} else {
process[writeStreamType ?? 'stdout'].write(formattedMessage);
}
});
}

Expand All @@ -352,7 +366,17 @@ export class ConsoleLogger implements LoggerService {
!this.options.colors && this.inspectOptions.compact === true
? JSON.stringify(logObject, this.stringifyReplacer)
: inspect(logObject, this.inspectOptions);
process[options.writeStreamType ?? 'stdout'].write(`${formattedMessage}\n`);
if (this.options.forceConsole) {
if (options.writeStreamType === 'stderr') {
console.error(formattedMessage);
} else {
console.log(formattedMessage);
}
} else {
process[options.writeStreamType ?? 'stdout'].write(
`${formattedMessage}\n`,
);
}
}

protected getJsonLogObject(
Expand Down Expand Up @@ -455,7 +479,11 @@ export class ConsoleLogger implements LoggerService {
if (!stack || this.options.json) {
return;
}
process.stderr.write(`${stack}\n`);
if (this.options.forceConsole) {
console.error(stack);
} else {
process.stderr.write(`${stack}\n`);
}
}

protected updateAndGetTimestampDiff(): string {
Expand Down
80 changes: 79 additions & 1 deletion packages/common/test/services/logger.service.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { expect } from 'chai';
import 'reflect-metadata';
import * as sinon from 'sinon';
import { ConsoleLogger, Logger, LoggerService, LogLevel } from '../../services';

Expand Down Expand Up @@ -475,6 +474,85 @@ describe('Logger', () => {
expect(processStdoutWriteSpy.secondCall.firstArg).to.include(Test.name);
});
});

describe('forceConsole option', () => {
let consoleLogSpy: sinon.SinonSpy;
let consoleErrorSpy: sinon.SinonSpy;
let processStdoutWriteStub: sinon.SinonStub;
let processStderrWriteStub: sinon.SinonStub;

beforeEach(() => {
// Stub process.stdout.write to prevent actual output and track calls
processStdoutWriteStub = sinon.stub(process.stdout, 'write');
processStderrWriteStub = sinon.stub(process.stderr, 'write');
consoleLogSpy = sinon.spy(console, 'log');
consoleErrorSpy = sinon.spy(console, 'error');
});

afterEach(() => {
processStdoutWriteStub.restore();
processStderrWriteStub.restore();
consoleLogSpy.restore();
consoleErrorSpy.restore();
});

it('should use console.log instead of process.stdout.write when forceConsole is true', () => {
const logger = new ConsoleLogger({ forceConsole: true });
const message = 'test message';

logger.log(message);

// When forceConsole is true, console.log should be called
expect(consoleLogSpy.called).to.be.true;
expect(consoleLogSpy.firstCall.firstArg).to.include(message);
});

it('should use console.error instead of process.stderr.write when forceConsole is true', () => {
const logger = new ConsoleLogger({ forceConsole: true });
const message = 'error message';

logger.error(message);

expect(consoleErrorSpy.called).to.be.true;
expect(consoleErrorSpy.firstCall.firstArg).to.include(message);
});

it('should use console.error for stack traces when forceConsole is true', () => {
const logger = new ConsoleLogger({ forceConsole: true });
const message = 'error with stack';
const stack = 'Error: test\n at <anonymous>:1:1';

logger.error(message, stack);

expect(consoleErrorSpy.calledTwice).to.be.true;
expect(consoleErrorSpy.firstCall.firstArg).to.include(message);
expect(consoleErrorSpy.secondCall.firstArg).to.equal(stack);
});

it('should use process.stdout.write when forceConsole is false', () => {
const logger = new ConsoleLogger({ forceConsole: false });
const message = 'test message';

logger.log(message);

expect(processStdoutWriteStub.called).to.be.true;
expect(processStdoutWriteStub.firstCall.firstArg).to.include(message);
expect(consoleLogSpy.called).to.be.false;
});

it('should work with JSON mode and forceConsole', () => {
const logger = new ConsoleLogger({ json: true, forceConsole: true });
const message = 'json message';

logger.log(message);

expect(consoleLogSpy.called).to.be.true;

const output = consoleLogSpy.firstCall.firstArg;
const json = JSON.parse(output);
expect(json.message).to.equal(message);
});
});
});

describe('[instance methods]', () => {
Expand Down
Loading