pino-nestjs v0.1.3
Keep your NestJS logs while gaining all the benefits of pino and pino-http: structured JSON logs, exceptional performance, and automatic request context tracking.
// Other loggers - violate NestJS parameter order
this.logger.log(context, 'message'); // ❌ context first, message second
// With pino-nestjs - respect NestJS parameter order
this.logger.log('message', context); // ✅ message first, context second
pino-nestjs
is a true drop-in replacement for NestJS's built-in logger.
Table of contents
- Installation
- Usage
- Comparison with other loggers
- Configuration
- Testing
- Extending logger classes
- Notes on logger injection
- Additional features
- Frequently asked questions
Installation
npm i pino-nestjs pino-http
Usage
Import the module
Import LoggerModule
in your root module:
import { LoggerModule } from 'pino-nestjs';
@Module({
imports: [LoggerModule.forRoot()],
})
class AppModule {}
Set up app logger
import { Logger } from 'pino-nestjs';
const app = await NestFactory.create(AppModule, { bufferLogs: true });
app.useLogger(app.get(Logger));
Use one of two logger options
Option 1: NestJS standard logger (recommended)
// NestJS standard built-in logger.
// Logs will be produced by pino internally
import { Logger } from '@nestjs/common';
export class MyService {
private readonly logger = new Logger(MyService.name);
foo() {
// NestJS parameter order: message first, then context (if needed)
// With pino-nestjs, you can use the standard NestJS logging pattern
this.logger.verbose('My verbose message', MyService.name);
this.logger.debug('User data processed', { userId: '123', status: 'success' });
this.logger.log('Operation completed', MyService.name);
// Object logging also works with NestJS parameter order
this.logger.warn({ operation: 'data_sync', status: 'warning' }, MyService.name);
// Error logging
try {
// Some operation
} catch (error) {
this.logger.error(error, error.stack, MyService.name);
}
}
}
Option 2: Direct Pino Logger
import { PinoLogger, InjectPinoLogger } from 'pino-nestjs';
export class MyService {
constructor(
private readonly logger: PinoLogger
) {
// Optionally set context in constructor
this.logger.setContext(MyService.name);
}
// Alternative: use decorator to set context
constructor(
@InjectPinoLogger(MyService.name)
private readonly logger: PinoLogger
) {}
foo() {
// When using PinoLogger directly, you still have access to Pino's native format
// But pino-nestjs also supports NestJS parameter order: message first, then context
this.logger.trace('This is a trace message');
this.logger.debug('Debug information', { userId: '123' });
this.logger.info('Information message', MyService.name);
// Traditional Pino object + message format also works
this.logger.trace({ operation: 'init' }, 'System initialized');
}
}
Log output example
// App logs
{"level":30,"time":1629823318326,"pid":14727,"hostname":"my-host","context":"NestFactory","msg":"Starting Nest application..."}
{"level":30,"time":1629823318326,"pid":14727,"hostname":"my-host","context":"InstanceLoader","msg":"LoggerModule dependencies initialized"}
{"level":30,"time":1629823318327,"pid":14727,"hostname":"my-host","context":"InstanceLoader","msg":"AppModule dependencies initialized"}
{"level":30,"time":1629823318327,"pid":14727,"hostname":"my-host","context":"RoutesResolver","msg":"AppController {/}:"}
{"level":30,"time":1629823318327,"pid":14727,"hostname":"my-host","context":"RouterExplorer","msg":"Mapped {/, GET} route"}
{"level":30,"time":1629823318327,"pid":14727,"hostname":"my-host","context":"NestApplication","msg":"Nest application successfully started"}
// Service logs with request context and req.id
{"level":10,"time":1629823792023,"pid":15067,"hostname":"my-host","req":{"id":1,"method":"GET","url":"/","query":{},"params":{"0":""},"headers":{"host":"localhost:3000","user-agent":"curl/7.64.1","accept":"*/*"},"remoteAddress":"::1","remotePort":63822},"context":"MyService","foo":"bar","msg":"baz qux"}
{"level":20,"time":1629823792023,"pid":15067,"hostname":"my-host","req":{"id":1,"method":"GET","url":"/","query":{},"params":{"0":""},"headers":{"host":"localhost:3000","user-agent":"curl/7.64.1","accept":"*/*"},"remoteAddress":"::1","remotePort":63822},"context":"MyService","msg":"foo bar {\"baz\":\"qux\"}"}
{"level":30,"time":1629823792023,"pid":15067,"hostname":"my-host","req":{"id":1,"method":"GET","url":"/","query":{},"params":{"0":""},"headers":{"host":"localhost:3000","user-agent":"curl/7.64.1","accept":"*/*"},"remoteAddress":"::1","remotePort":63822},"context":"MyService","msg":"foo"}
// Automatic request/response logs
{"level":30,"time":1629823792029,"pid":15067,"hostname":"my-host","req":{"id":1,"method":"GET","url":"/","query":{},"params":{"0":""},"headers":{"host":"localhost:3000","user-agent":"curl/7.64.1","accept":"*/*"},"remoteAddress":"::1","remotePort":63822},"res":{"statusCode":200,"headers":{"x-powered-by":"Express","content-type":"text/html; charset=utf-8","content-length":"12","etag":"W/\"c-Lve95gjOVATpfV8EL5X4nxwjKHE\""}},"responseTime":7,"msg":"request completed"}
Comparison with other loggers
Key features of this module:
- Idiomatic NestJS logger
- JSON format logging (via pino - super fast logger)
- Automatic request/response logging (via pino-http)
- Request data binding to logs from any service without passing context (via AsyncLocalStorage)
- Alternative
PinoLogger
with the same API aspino
for experienced pino users - NestJS compatible parameter order (message first, context second) for direct compatibility with NestJS Logger
Differences from nestjs-pino
pino-nestjs
is a fork of nestjs-pino with one key improvement: parameter order compatibility with NestJS.
See nestjs-pino#2004 to understand the motivation for this fork.
// With nestjs-pino (Pino style):
this.logger.log({ foo: 'bar' }, 'Hello World');
// With pino-nestjs (NestJS style):
this.logger.log('Hello World', { foo: 'bar' });
Unlike nestjs-pino which follows Pino's convention (context first, then message), pino-nestjs
acts as a true drop-in replacement for NestJS's built-in Logger by using the NestJS parameter order (message first, then context).
Logger | Nest App Logger | Logger Service | Auto-bind Request Data | NestJS Parameter Order |
---|---|---|---|---|
nest-winston | ✅ | ✅ | ❌ | ✅ |
nestjs-pino-logger | ✅ | ✅ | ❌ | ✅ |
nestjs-pino | ✅ | ✅ | ✅ | ❌ |
pino-nestjs | ✅ | ✅ | ✅ | ✅ |
Configuration
Zero configuration
import { LoggerModule } from 'pino-nestjs';
@Module({
imports: [LoggerModule.forRoot()],
// ...
})
class MyModule {}
Configuration parameters
interface Params {
/**
* Optional parameters for `pino-http` module
* @see https://github.com/pinojs/pino-http#api
*/
pinoHttp?:
| pinoHttp.Options
| DestinationStream
| [pinoHttp.Options, DestinationStream];
/**
* Optional parameter for routing. Implements interface of
* NestJS built-in `MiddlewareConfigProxy['forRoutes']`.
* @see https://docs.nestjs.com/middleware#applying-middleware
*/
forRoutes?: Parameters<MiddlewareConfigProxy['forRoutes']>;
/**
* Optional parameter for routing. Implements interface of
* NestJS built-in `MiddlewareConfigProxy['exclude']`.
* @see https://docs.nestjs.com/middleware#applying-middleware
*/
exclude?: Parameters<MiddlewareConfigProxy['exclude']>;
/**
* Optional parameter to skip pino configuration when using
* FastifyAdapter with pre-configured logger.
* @see https://github.com/yamcodes/pino-nestjs#faq
*/
useExisting?: true;
/**
* Optional parameter to change property name `context` in logs
*/
renameContext?: string;
}
Synchronous configuration
import { LoggerModule } from 'pino-nestjs';
@Module({
imports: [
LoggerModule.forRoot({
pinoHttp: [
{
name: 'my-app-name',
level: process.env.NODE_ENV !== 'production' ? 'debug' : 'info',
// Install 'pino-pretty' package to use this option
transport: process.env.NODE_ENV !== 'production'
? { target: 'pino-pretty' }
: undefined,
// Other pino-http options:
// https://github.com/pinojs/pino-http#api
// https://github.com/pinojs/pino/blob/HEAD/docs/api.md#options-object
},
someWritableStream
],
forRoutes: [MyController],
exclude: [{ method: RequestMethod.ALL, path: 'check' }]
})
],
// ...
})
class MyModule {}
Asynchronous configuration
import { LoggerModule } from 'pino-nestjs';
@Injectable()
class ConfigService {
public readonly level = 'debug';
}
@Module({
providers: [ConfigService],
exports: [ConfigService]
})
class ConfigModule {}
@Module({
imports: [
LoggerModule.forRootAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: async (config: ConfigService) => {
await somePromise();
return {
pinoHttp: { level: config.level },
};
}
})
],
// ...
})
class TestModule {}
Asynchronous logging
Asynchronous logging enables even faster performance by pino but risks losing the most recently buffered logs in case of system failure.
Read pino asynchronous mode docs first.
import pino from 'pino';
import { LoggerModule } from 'pino-nestjs';
@Module({
imports: [
LoggerModule.forRoot({
pinoHttp: {
stream: pino.destination({
dest: './my-file', // omit for stdout
minLength: 4096, // Buffer before writing
sync: false, // Asynchronous logging
}),
},
}),
],
// ...
})
class MyModule {}
See pino.destination
Testing a class that uses @InjectPinoLogger
The package exposes a getLoggerToken()
function that returns an injection token based on the provided context:
const module: TestingModule = await Test.createTestingModule({
providers: [
MyService,
{
provide: getLoggerToken(MyService.name),
useValue: mockLogger,
},
],
}).compile();
Logger and PinoLogger class extension
Both classes can be extended:
// logger.service.ts
import { Logger, PinoLogger, Params, PARAMS_PROVIDER_TOKEN } from 'pino-nestjs';
@Injectable()
class LoggerService extends Logger {
constructor(
logger: PinoLogger,
@Inject(PARAMS_PROVIDER_TOKEN) params: Params
) {
super();
// ...
}
// Extended method
myMethod(): any {}
}
// Alternative: Extend PinoLogger
@Injectable()
class LoggerService extends PinoLogger {
constructor(
@Inject(PARAMS_PROVIDER_TOKEN) params: Params
) {
super();
// ...
}
// Extended method
myMethod(): any {}
}
// logger.module.ts
@Module({
providers: [LoggerService],
exports: [LoggerService],
imports: [LoggerModule.forRoot()],
})
class LoggerModule {}
Notes on logger injection in constructor
Since NestJS@8, the main purpose of the Logger
class is to be registered via app.useLogger(app.get(Logger))
. With this usage, NestJS passes the logger's context as the last optional argument in logging functions.
This creates a limitation in detecting whether a method was called by app internals (where the last argument is context) or by an injected Logger
instance in a service (where the last argument might be an interpolation value).
Reuse the Fastify logger configuration
!WARNING This feature is not recommended for most cases. Read on to understand the caveats.
If you use useExisting: true
with Fastify, you can reuse the Fastify logger configuration by providing the same options in forRoot
/forRootAsync
:
import { LoggerModule } from 'pino-nestjs';
@Module({
imports: [
LoggerModule.forRoot({
useExisting: true,
}),
],
})
class MyModule {}
When working with Fastify and pino-nestjs together, you need to understand how logger instances are managed:
Request vs. Application Context:
- Fastify creates a logger with your configuration for each request
- NestJS has additional execution contexts (like lifecycle events) that occur outside request context
- For these non-request contexts,
Logger
/PinoLogger
services use a separate pino instance configured viaforRoot
/forRootAsync
Configuration Sharing Issues:
- When configuring pino via
FastifyAdapter
, there's no way to extract that configuration and apply it to the out-of-context logger - Without explicit configuration in
forRoot
/forRootAsync
, the out-of-context logger will use default parameters
- When configuring pino via
Potential Solutions:
- For consistency, you must provide identical configurations to both Fastify and LoggerModule
- Better approach: configure only through LoggerModule and drop the
useExisting
option entirely
When to use
useExisting: true
:- Only when you don't need logging for lifecycle events and application-level logging
- Only when using pino with default parameters in Fastify-based NestJS apps
For all other scenarios, using useExisting: true
will lead to either code duplication or unexpected behavior.
Assign extra fields for future calls
You can enrich logs using the assign
method of PinoLogger
:
@Controller('/')
class TestController {
constructor(
private readonly logger: PinoLogger,
private readonly service: MyService,
) {}
@Get()
get() {
// Assign extra fields in one place...
this.logger.assign({ userID: '42' });
return this.service.test();
}
}
@Injectable()
class MyService {
private readonly logger = new Logger(MyService.name);
test() {
// ...and it will be logged in another place
this.logger.log('hello world');
}
}
Set the assignResponse
parameter to true
to also enrich request completion logs.
Change Pino parameters at runtime
You can modify pino root logger parameters at runtime:
@Controller('/')
class TestController {
@Post('/change-logging-level')
setLevel() {
PinoLogger.root.level = 'info';
return null;
}
}
Expose stack trace and error class in err
property
Use the provided interceptor to expose actual error details:
import { LoggerErrorInterceptor } from 'pino-nestjs';
const app = await NestFactory.create(AppModule);
app.useGlobalInterceptors(new LoggerErrorInterceptor());
Frequently asked questions
Q: How do I disable automatic request/response logs?
A: Use the autoLogging field of pino-http in the pinoHttp
configuration.
Q: How do I pass X-Request-ID
header or generate UUID for req.id
?
A: Use the genReqId field of pino-http in the pinoHttp
configuration.
Q: How does it work?
A: It uses pino-http to create a child-logger for each request, and with AsyncLocalStorage, Logger
and PinoLogger
can access it from any service. This allows logs to be grouped by req.id
.
Q: Why use AsyncLocalStorage instead of REQUEST scope?
A: REQUEST scope can have performance issues as it creates new instances of each service per request.
Q: What about pino
built-in methods/levels?
A: Here's the mapping between methods:
pino | PinoLogger | NestJS Logger |
---|---|---|
trace | trace | verbose |
debug | debug | debug |
info | info | log |
warn | warn | warn |
error | error | error |
fatal | fatal | fatal (since nestjs@10.2) |
Q: I use Fastify and want to configure pino at the Adapter level. Can I use that config for the logger?
A: You can use useExisting: true
, but there are caveats.