Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
163 changes: 163 additions & 0 deletions integration/hello-world/e2e/force-console.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
import { ConsoleLogger, INestApplication } from '@nestjs/common';
import { Test } from '@nestjs/testing';
import * as request from 'supertest';
import { AppModule } from '../src/app.module';
import * as sinon from 'sinon';
import { expect } from 'chai';

describe('ForceConsole Option', () => {
let app: INestApplication;

describe('When forceConsole is true', () => {
let consoleLogSpy: sinon.SinonSpy;
let consoleErrorSpy: sinon.SinonSpy;
let processStdoutSpy: sinon.SinonSpy;
let processStderrSpy: sinon.SinonSpy;

beforeEach(async () => {
// Spy on console and process methods
consoleLogSpy = sinon.spy(console, 'log');
consoleErrorSpy = sinon.spy(console, 'error');
processStdoutSpy = sinon.spy(process.stdout, 'write');
processStderrSpy = sinon.spy(process.stderr, 'write');

const moduleRef = await Test.createTestingModule({
imports: [AppModule],
}).compile();

app = moduleRef.createNestApplication({
forceConsole: true,
logger: ['log', 'error'],
});

await app.init();
});

afterEach(async () => {
consoleLogSpy.restore();
consoleErrorSpy.restore();
processStdoutSpy.restore();
processStderrSpy.restore();
await app.close();
});

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

// Should use console.log when forceConsole is true
expect(consoleLogSpy.called).to.be.true;
// Verify console.log was called with the message
const consoleLogCalls = consoleLogSpy
.getCalls()
.filter(call =>
call.args.some(arg => String(arg).includes('Test log message')),
);
expect(consoleLogCalls.length).to.be.greaterThan(0);
});

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

// Should use console.error when forceConsole is true
expect(consoleErrorSpy.called).to.be.true;
// Verify console.error was called with the message
const consoleErrorCalls = consoleErrorSpy
.getCalls()
.filter(call =>
call.args.some(arg => String(arg).includes('Test error message')),
);
expect(consoleErrorCalls.length).to.be.greaterThan(0);
});

it('should handle GET request with forceConsole option enabled', () => {
return request(app.getHttpServer()).get('/hello').expect(200);
});
});

describe('When forceConsole is false (default)', () => {
let consoleLogSpy: sinon.SinonSpy;
let consoleErrorSpy: sinon.SinonSpy;
let processStdoutSpy: sinon.SinonSpy;
let processStderrSpy: sinon.SinonSpy;

beforeEach(async () => {
// Spy on console and process methods
consoleLogSpy = sinon.spy(console, 'log');
consoleErrorSpy = sinon.spy(console, 'error');
processStdoutSpy = sinon.spy(process.stdout, 'write');
processStderrSpy = sinon.spy(process.stderr, 'write');

const moduleRef = await Test.createTestingModule({
imports: [AppModule],
}).compile();

app = moduleRef.createNestApplication({
logger: ['log', 'error'],
// forceConsole is not set, defaults to false
});

await app.init();
});

afterEach(async () => {
consoleLogSpy.restore();
consoleErrorSpy.restore();
processStdoutSpy.restore();
processStderrSpy.restore();
await app.close();
});

it('should not directly call console.log when forceConsole is false', async () => {
const logger = new ConsoleLogger('TestContext');

// Reset spy to ensure clean state
consoleLogSpy.resetHistory();

logger.log('Test log message');

// When forceConsole is false, should not call console.log
expect(consoleLogSpy.called).to.be.false;
});

it('should not directly call console.error when forceConsole is false', async () => {
const logger = new ConsoleLogger('TestContext');

// Reset spy to ensure clean state
consoleErrorSpy.resetHistory();

logger.error('Test error message');

// When forceConsole is false, should not call console.error
expect(consoleErrorSpy.called).to.be.false;
});
});

describe('When forceConsole is set via NestFactory.create', () => {
it('should apply forceConsole to the default logger', async () => {
const consoleLogSpy = sinon.spy(console, 'log');
const processStdoutSpy = sinon.spy(process.stdout, 'write');

const moduleRef = await Test.createTestingModule({
imports: [AppModule],
}).compile();

const testApp = moduleRef.createNestApplication({
forceConsole: true,
});

await testApp.init();

// The logger created by NestFactory should respect forceConsole option
const logger = new ConsoleLogger('AppContext', { forceConsole: true });
logger.log('Application started');

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

consoleLogSpy.restore();
processStdoutSpy.restore();
await testApp.close();
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -67,4 +67,11 @@ export class NestApplicationContextOptions {
*/
instanceDecorator: (instance: unknown) => unknown;
};

/**
* If enabled, will force the use of console.log/console.error instead of process.stdout/stderr.write
* in the default ConsoleLogger. This is useful for test environments like Jest that can buffer console calls.
* @default false
*/
forceConsole?: boolean;
}
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
8 changes: 7 additions & 1 deletion packages/core/nest-factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
import { NestMicroserviceOptions } from '@nestjs/common/interfaces/microservices/nest-microservice-options.interface';
import { NestApplicationContextOptions } from '@nestjs/common/interfaces/nest-application-context-options.interface';
import { NestApplicationOptions } from '@nestjs/common/interfaces/nest-application-options.interface';
import { ConsoleLogger } from '@nestjs/common/services/console-logger.service';
import { Logger } from '@nestjs/common/services/logger.service';
import { loadPackage } from '@nestjs/common/utils/load-package.util';
import { isFunction, isNil } from '@nestjs/common/utils/shared.utils';
Expand Down Expand Up @@ -299,9 +300,14 @@ export class NestFactoryStatic {
if (!options) {
return;
}
const { logger, bufferLogs, autoFlushLogs } = options;
const { logger, bufferLogs, autoFlushLogs, forceConsole } = options;
if ((logger as boolean) !== true && !isNil(logger)) {
Logger.overrideLogger(logger);
} else if (forceConsole) {
// If no custom logger is provided but forceConsole is true,
// create a ConsoleLogger with forceConsole option
const consoleLogger = new ConsoleLogger({ forceConsole: true });
Logger.overrideLogger(consoleLogger);
}
if (bufferLogs) {
Logger.attachBuffer();
Expand Down
Loading