Repository: iamolegga/nestjs-pino
Pull request #27 (2019-10-18 15:44:09)

PinoLogger class
implements #22 


Patch:
diff --git a/README.md b/README.md
index a273931..f42534b 100644
--- a/README.md
+++ b/README.md
@@ -26,7 +26,7 @@
 
 ## Example
 
-Import module:
+Import module with `LoggerModule.forRoot(...)` or `LoggerModule.forRootAsync(...)`:
 
 ```ts
 import { LoggerModule } from 'nestjs-pino';
@@ -39,7 +39,7 @@ import { LoggerModule } from 'nestjs-pino';
 class MyModule {}
 ```
 
-In controller:
+In controller let's use `Logger` - class with the same API as [built-in NestJS logger](https://docs.nestjs.com/techniques/logger):
 
 ```ts
 import { Logger } from 'nestjs-pino';
@@ -53,38 +53,91 @@ export class AppController {
 
   @Get()
   getHello(): string {
+
+    // pass message
+    this.logger.log("getHello()");
+
+    // also we can pass context
     this.logger.log("getHello()", AppController.name);
+
     return `Hello ${this.myService.getWorld()}`;
   }
 }
 ```
 
-In service:
+Let's compare it to another one logger - `PinoLogger`, it has same _logging_ API as `pino` instance.
+
+For example in service it will be used instead of previous one:
 
 ```ts
-import { Logger } from 'nestjs-pino';
+import { PinoLogger } from 'nestjs-pino';
 
 @Injectable()
 export class MyService {
-  constructor(private readonly logger: Logger) {}
+  constructor(private readonly logger: PinoLogger) {}
 
   getWorld(...params: any[]) {
-    this.logger.log("getWorld(%o)", MyService.name, params);
+    this.logger.info({ context: MyService.name }, "getWorld(%o)", params);
+    return "World!";
+  }
+}
+```
+
+Also context can be set just once in `constructor` instead of every call:
+
+```ts
+import { PinoLogger } from 'nestjs-pino';
+
+@Injectable()
+export class MyService {
+  constructor(private readonly logger: PinoLogger) {
+    logger.setContext(MyService.name);
+  }
+
+  getWorld(...params: any[]) {
+    this.logger.info("getWorld(%o)", params);
+    return "World!";
+  }
+}
+```
+
+Also context can be set at injection via decorator `@InjectPinoLogger(...)`:
+
+```ts
+import { PinoLogger, InjectPinoLogger } from 'nestjs-pino';
+
+@Injectable()
+export class MyService {
+  constructor(
+    @InjectPinoLogger(MyService.name) private readonly logger: PinoLogger) {
+  }
+
+  getWorld(...params: any[]) {
+    this.logger.info("getWorld(%o)", params);
     return "World!";
   }
 }
 ```
 
+And also `Logger` can be set as app logger, as it is compatible with [built-in NestJS logger](https://docs.nestjs.com/techniques/logger):
+
+```ts
+import { Logger } from 'nestjs-pino';
+
+const app = await NestFactory.create(AppModule, { logger: false });
+app.useLogger(app.get(Logger));
+```
+
 Output:
 
 ```json
-// Logs by Nest itself, when set `app.useLogger(app.get(Logger))`
+// Logs by app itself
 {"level":30,"time":1570470154387,"pid":17383,"hostname":"my-host","context":"RoutesResolver","msg":"AppController {/}: true","v":1}
 {"level":30,"time":1570470154391,"pid":17383,"hostname":"my-host","context":"RouterExplorer","msg":"Mapped {/, GET} route true","v":1}
 {"level":30,"time":1570470154405,"pid":17383,"hostname":"my-host","context":"NestApplication","msg":"Nest application successfully started true","v":1}
 
-// Logs by injected Logger methods in Services/Controllers
-// Every log has it's request data
+// Logs by injected Logger and PinoLogger in Services/Controllers
+// Every log has it's request data and unique `req.id` (per )
 {"level":30,"time":1570470161805,"pid":17383,"hostname":"my-host","req":{"id":1,"method":"GET","url":"/","headers":{...},"remoteAddress":"::1","remotePort":53957},"context":"AppController","msg":"getHello()","v":1}
 {"level":30,"time":1570470161805,"pid":17383,"hostname":"my-host","req":{"id":1,"method":"GET","url":"/","headers":{...},"remoteAddress":"::1","remotePort":53957},"context":"MyService","msg":"getWorld([])","v":1}
 
@@ -94,11 +147,12 @@ Output:
 
 ## Comparison with others
 
-There are other Nestjs loggers. The key purposes of this one are:
+There are other Nestjs loggers. The key purposes of this module are:
   - to be compatible with built-in `LoggerService`
   - to log with JSON (thanks to `pino` - [super fast logger](https://github.com/pinojs/pino/blob/master/docs/benchmarks.md)) ([why JSON?](https://jahed.dev/2018/07/05/always-log-to-json/))
   - to log every request/response automatically (thanks to `pino-http`)
   - to bind request data to the logs automatically from any service on any application layer without passing request context
+  - to have another one alternative `PinoLogger` for experienced `pino` users for more comfortable usage.
 
 | Logger             | Nest App logger | Logger service | Autobind request data to logs |
 | ------------------ | :-------------: | :------------: | :---------------------------: |
@@ -245,7 +299,12 @@ class MyModule {}
 
 ## Usage as Logger service
 
-`Logger` implements standard NestJS `LoggerService` interface. So if you are familiar with [built-in NestJS logger](https://docs.nestjs.com/techniques/logger), you are good to go.
+As it said before, there are 2 logger classes:
+
+- `Logger` - implements standard NestJS `LoggerService` interface. So if you are familiar with [built-in NestJS logger](https://docs.nestjs.com/techniques/logger), you are good to go.
+- `PinoLogger` - implements standard `pino` _logging_ methods: [trace](https://github.com/pinojs/pino/blob/master/docs/api.md#loggertracemergingobject-message-interpolationvalues), [debug](https://github.com/pinojs/pino/blob/master/docs/api.md#loggerdebugmergingobject-message-interpolationvalues), [info](https://github.com/pinojs/pino/blob/master/docs/api.md#loggerinfomergingobject-message-interpolationvalues), [warn](https://github.com/pinojs/pino/blob/master/docs/api.md#loggerwarnmergingobject-message-interpolationvalues), [error](https://github.com/pinojs/pino/blob/master/docs/api.md#loggererrormergingobject-message-interpolationvalues), [fatal](https://github.com/pinojs/pino/blob/master/docs/api.md#loggerfatalmergingobject-message-interpolationvalues). So if you are familiar with it, you are also good to go.
+
+### Logger
 
 ```ts
 // my.service.ts
@@ -262,6 +321,38 @@ export class MyService {
 }
 ```
 
+### PinoLogger
+
+See [pino logging method parameters](https://github.com/pinojs/pino/blob/master/docs/api.md#logging-method-parameters) for more logging examples.
+
+```ts
+// my.service.ts
+import { PinoLogger, InjectPinoLogger } from 'nestjs-pino';
+
+@Injectable()
+export class MyService {
+
+  // regular injecting
+  constructor(private readonly logger: PinoLogger) {}
+
+  // regular injecting and set context
+  constructor(private readonly logger: PinoLogger) {
+    logger.setContext(MyService.name);
+  }
+
+  // inject and set context via `InjectPinoLogger`
+  constructor(
+    @InjectPinoLogger(MyService.name) private readonly logger: PinoLogger
+  ) {}
+
+
+  getWorld(...params: any[]) {
+    this.logger.info("getWorld(%o)", params);
+    return "World!";
+  }
+}
+```
+
 ## Usage as NestJS app logger
 
 According to [official docs](https://docs.nestjs.com/techniques/logger#dependency-injection), loggers with Dependency injection should be set via following construction:
@@ -277,24 +368,31 @@ app.useLogger(app.get(Logger));
 
 __Q__: _How does it work?_
 
-__A__: It uses [pino-http](https://github.com/pinojs/pino-http) under hood, so every request has it's own [child-logger](https://github.com/pinojs/pino/blob/master/docs/child-loggers.md), and with help of [async_hooks](https://nodejs.org/api/async_hooks.html) `Logger` can get it while calling own methods. So your logs can be grouped by `req.id`.
+__A__: It uses [pino-http](https://github.com/pinojs/pino-http) under hood, so every request has it's own [child-logger](https://github.com/pinojs/pino/blob/master/docs/child-loggers.md), and with help of [async_hooks](https://nodejs.org/api/async_hooks.html) `Logger` and `PinoLogger` can get it while calling own methods. So your logs can be grouped by `req.id`. If you run several instances, unique key is pair: `pid` + `req.id`.
+
+---
 
 __Q__: _Why use [async_hooks](https://nodejs.org/api/async_hooks.html) instead of [REQUEST scope](https://docs.nestjs.com/fundamentals/injection-scopes#per-request-injection)?_
 
 __A__: [REQUEST scope](https://docs.nestjs.com/fundamentals/injection-scopes#per-request-injection) can have [perfomance issues](https://docs.nestjs.com/fundamentals/injection-scopes#performance). TL;DR: it will have to create an instance of the class (that injects `Logger`) on each request, and that will slow down your responce times.
 
+---
+
 __Q__: _I'm using old nodejs version, will it work for me?_
 
 __A__: Please read [this](https://github.com/jeff-lewis/cls-hooked#continuation-local-storage--hooked-).
 
+---
+
 __Q__: _What about pino built-in methods/levels?_
 
-__A__: Pino built-in methods are not compatible with NestJS built-in `LoggerService` methods. Decision was made to support the latter and map them accordingly:
+__A__: Pino built-in methods are not compatible with NestJS built-in `LoggerService` methods. So for now there is option which logger to use, here is methods mapping:
 
-| pino    | LoggerService |
-| ------- | ------------- |
-| `trace` | `verbose`     |
-| `debug` | `debug`       |
-| `info`  | `log`         |
-| `warn`  | `warn`        |
-| `error` | `error`       |
+| `pino` method | `PinoLogger` method | `Logger` method |
+| ------------- | ------------------- | --------------- |
+| trace         | trace               | **verbose**     |
+| debug         | debug               | debug           |
+| info          | info                | **log**         |
+| warn          | warn                | warn            |
+| error         | error               | error           |
+| fatal         | fatal               | -               |
diff --git a/example/app.controller.ts b/example/app.controller.ts
index 1995722..de7b4ab 100644
--- a/example/app.controller.ts
+++ b/example/app.controller.ts
@@ -1,17 +1,19 @@
 import { Controller, Get } from "@nestjs/common";
 import { MyService } from "./my.service";
-import { Logger } from "../src";
+import { PinoLogger } from "../src";
 
 @Controller()
 export class AppController {
   constructor(
     private readonly myService: MyService,
-    private readonly logger: Logger
-  ) {}
+    private readonly logger: PinoLogger
+  ) {
+    logger.setContext(AppController.name);
+  }
 
   @Get()
   getHello(): string {
-    this.logger.log("getHello()", AppController.name);
+    this.logger.info("getHello(%O)", arguments);
     return `Hello ${this.myService.getWorld()}`;
   }
 }
diff --git a/example/my.service.ts b/example/my.service.ts
index 97e9456..4155c5a 100644
--- a/example/my.service.ts
+++ b/example/my.service.ts
@@ -1,9 +1,11 @@
 import { Injectable } from "@nestjs/common";
-import { Logger } from "../src";
+import { PinoLogger, InjectPinoLogger } from "../src";
 
 @Injectable()
 export class MyService {
-  constructor(private readonly logger: Logger) {}
+  constructor(
+    @InjectPinoLogger(MyService.name) private readonly logger: PinoLogger
+  ) {}
 
   getWorld(...params: any[]) {
     this.logger.debug("getWorld(%o)", MyService.name, params);
diff --git a/package-lock.json b/package-lock.json
index bccf096..8602de4 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,6 +1,6 @@
 {
   "name": "nestjs-pino",
-  "version": "0.4.0",
+  "version": "0.5.0",
   "lockfileVersion": 1,
   "requires": true,
   "dependencies": {
diff --git a/package.json b/package.json
index 896718b..96f3f59 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "nestjs-pino",
-  "version": "0.4.0",
+  "version": "0.5.0",
   "description": "Pino logger for NestJS",
   "main": "dist/index.js",
   "scripts": {
diff --git a/src/InjectPinoLogger.ts b/src/InjectPinoLogger.ts
new file mode 100644
index 0000000..767a6a3
--- /dev/null
+++ b/src/InjectPinoLogger.ts
@@ -0,0 +1,28 @@
+import { Inject, Provider } from "@nestjs/common";
+import { PinoLogger } from "./PinoLogger";
+
+const decoratedTokenPrefix = "PinoLogger:";
+
+const decoratedLoggers = new Set<string>();
+
+export function InjectPinoLogger(context = "") {
+  decoratedLoggers.add(context);
+  return Inject(`${decoratedTokenPrefix}${context}`);
+}
+
+function createDecoratedLoggerProvider(context: string): Provider<PinoLogger> {
+  return {
+    provide: `${decoratedTokenPrefix}${context}`,
+    useFactory: (logger: PinoLogger) => {
+      logger.setContext(context);
+      return logger;
+    },
+    inject: [PinoLogger]
+  };
+}
+
+export function createProvidersForDecorated(): Array<Provider<PinoLogger>> {
+  return [...decoratedLoggers.values()].map(context =>
+    createDecoratedLoggerProvider(context)
+  );
+}
diff --git a/src/Logger.ts b/src/Logger.ts
new file mode 100644
index 0000000..1428505
--- /dev/null
+++ b/src/Logger.ts
@@ -0,0 +1,49 @@
+import { Injectable, LoggerService } from "@nestjs/common";
+import { PinoLogger } from "./PinoLogger";
+
+@Injectable()
+export class Logger implements LoggerService {
+  constructor(private readonly logger: PinoLogger) {}
+
+  verbose(message: any, context?: string, ...args: any[]) {
+    if (context) {
+      this.logger.trace({ context }, message, ...args);
+    } else {
+      this.logger.trace(message, ...args);
+    }
+  }
+
+  debug(message: any, context?: string, ...args: any[]) {
+    if (context) {
+      this.logger.debug({ context }, message, ...args);
+    } else {
+      this.logger.debug(message, ...args);
+    }
+  }
+
+  log(message: any, context?: string, ...args: any[]) {
+    if (context) {
+      this.logger.info({ context }, message, ...args);
+    } else {
+      this.logger.info(message, ...args);
+    }
+  }
+
+  warn(message: any, context?: string, ...args: any[]) {
+    if (context) {
+      this.logger.warn({ context }, message, ...args);
+    } else {
+      this.logger.warn(message, ...args);
+    }
+  }
+
+  error(message: any, trace?: string, context?: string, ...args: any[]) {
+    if (context) {
+      this.logger.error({ context, trace }, message, ...args);
+    } else if (trace) {
+      this.logger.error({ trace }, message, ...args);
+    } else {
+      this.logger.error(message, ...args);
+    }
+  }
+}
diff --git a/src/LoggerCoreModule.ts b/src/LoggerCoreModule.ts
new file mode 100644
index 0000000..d3f30cd
--- /dev/null
+++ b/src/LoggerCoreModule.ts
@@ -0,0 +1,89 @@
+import {
+  Global,
+  Module,
+  DynamicModule,
+  NestModule,
+  MiddlewareConsumer,
+  RequestMethod,
+  Inject
+} from "@nestjs/common";
+import { Provider } from "@nestjs/common/interfaces";
+import * as express from "express";
+import * as pinoHttp from "pino-http";
+import { setValue, middleware as ctxMiddleware } from "express-ctx";
+import { Logger } from "./Logger";
+import { OPTIONS_PROVIDER_TOKEN, LOGGER_KEY } from "./constants";
+import { Params, LoggerModuleAsyncOptions } from "./params";
+import { PinoLogger } from "./PinoLogger";
+import { createProvidersForDecorated } from "./InjectPinoLogger";
+
+@Global()
+@Module({ providers: [Logger], exports: [Logger] })
+export class LoggerCoreModule implements NestModule {
+  static forRoot(options: Params | undefined): DynamicModule {
+    const optionsProvider: Provider<Params> = {
+      provide: OPTIONS_PROVIDER_TOKEN,
+      useValue: options || null
+    };
+
+    const decorated = createProvidersForDecorated();
+
+    return {
+      module: LoggerCoreModule,
+      providers: [Logger, ...decorated, PinoLogger, optionsProvider],
+      exports: [Logger, ...decorated, PinoLogger]
+    };
+  }
+
+  static forRootAsync(options: LoggerModuleAsyncOptions): DynamicModule {
+    const optionsProvider: Provider<Params | Promise<Params>> = {
+      provide: OPTIONS_PROVIDER_TOKEN,
+      useFactory: options.useFactory,
+      inject: options.inject
+    };
+
+    const decorated = createProvidersForDecorated();
+
+    const providers: any[] = [
+      Logger,
+      ...decorated,
+      PinoLogger,
+      optionsProvider,
+      ...(options.providers || [])
+    ];
+
+    return {
+      module: LoggerCoreModule,
+      imports: options.imports,
+      providers,
+      exports: [Logger, ...decorated, PinoLogger]
+    };
+  }
+
+  constructor(
+    @Inject(OPTIONS_PROVIDER_TOKEN) private readonly options: Params
+  ) {}
+
+  configure(consumer: MiddlewareConsumer) {
+    consumer
+      .apply(...createLoggerMiddlewares(this.options))
+      .forRoutes({ path: "*", method: RequestMethod.ALL });
+  }
+}
+
+function createLoggerMiddlewares(params: Params) {
+  const middleware = Array.isArray(params)
+    ? pinoHttp(...params)
+    : pinoHttp((params as pinoHttp.Options) || undefined);
+
+  return [ctxMiddleware, middleware, bindLoggerMiddleware];
+}
+
+function bindLoggerMiddleware(
+  req: express.Request,
+  res: express.Response,
+  next: express.NextFunction
+) {
+  setValue(LOGGER_KEY, req.log);
+  next();
+}
diff --git a/src/LoggerModule.ts b/src/LoggerModule.ts
new file mode 100644
index 0000000..e745f2b
--- /dev/null
+++ b/src/LoggerModule.ts
@@ -0,0 +1,32 @@
+import { Module, DynamicModule } from "@nestjs/common";
+import { DestinationStream, LoggerOptions } from "pino";
+import { PassedLogger, LoggerModuleAsyncOptions } from "./params";
+import { LoggerCoreModule } from "./LoggerCoreModule";
+
+@Module({})
+export class LoggerModule {
+  static forRoot(
+    opts?: PassedLogger | LoggerOptions | DestinationStream
+  ): DynamicModule;
+  static forRoot(opts: LoggerOptions, stream: DestinationStream): DynamicModule;
+  static forRoot(
+    opts?: PassedLogger | LoggerOptions | DestinationStream,
+    stream?: DestinationStream
+  ): DynamicModule {
+    return {
+      module: LoggerModule,
+      imports: [
+        LoggerCoreModule.forRoot(
+          stream ? [opts as LoggerOptions, stream] : opts
+        )
+      ]
+    };
+  }
+
+  static forRootAsync(options: LoggerModuleAsyncOptions): DynamicModule {
+    return {
+      module: LoggerModule,
+      imports: [LoggerCoreModule.forRootAsync(options)]
+    };
+  }
+}
diff --git a/src/PinoLogger.ts b/src/PinoLogger.ts
new file mode 100644
index 0000000..df4315b
--- /dev/null
+++ b/src/PinoLogger.ts
@@ -0,0 +1,98 @@
+import { Injectable, Inject, Scope } from "@nestjs/common";
+import * as pino from "pino";
+import { getValue } from "express-ctx";
+import { OPTIONS_PROVIDER_TOKEN, LOGGER_KEY } from "./constants";
+import { Params, isPassedLogger } from "./params";
+
+interface PinoMethods
+  extends Pick<
+    pino.BaseLogger,
+    "trace" | "debug" | "info" | "warn" | "error" | "fatal"
+  > {}
+
+type LoggerFn =
+  | ((msg: string, ...args: any[]) => void)
+  | ((obj: object, msg?: string, ...args: any[]) => void);
+
+let outOfContext: pino.Logger;
+
+export function __resetOutOfContextForTests() {
+  // only for tests
+  // @ts-ignore
+  outOfContext = undefined;
+}
+
+@Injectable({ scope: Scope.TRANSIENT })
+export class PinoLogger implements PinoMethods {
+  private context = "";
+
+  constructor(@Inject(OPTIONS_PROVIDER_TOKEN) options: Params) {
+    if (!outOfContext) {
+      if (Array.isArray(options)) {
+        outOfContext = pino(...options);
+      } else if (isPassedLogger(options)) {
+        outOfContext = options.logger;
+      } else {
+        outOfContext = pino(options || undefined);
+      }
+    }
+  }
+
+  trace(msg: string, ...args: any[]): void;
+  trace(obj: object, msg?: string, ...args: any[]): void;
+  trace(...args: Parameters<LoggerFn>) {
+    this.call("trace", ...args);
+  }
+
+  debug(msg: string, ...args: any[]): void;
+  debug(obj: object, msg?: string, ...args: any[]): void;
+  debug(...args: Parameters<LoggerFn>) {
+    this.call("debug", ...args);
+  }
+
+  info(msg: string, ...args: any[]): void;
+  info(obj: object, msg?: string, ...args: any[]): void;
+  info(...args: Parameters<LoggerFn>) {
+    this.call("info", ...args);
+  }
+
+  warn(msg: string, ...args: any[]): void;
+  warn(obj: object, msg?: string, ...args: any[]): void;
+  warn(...args: Parameters<LoggerFn>) {
+    this.call("warn", ...args);
+  }
+
+  error(msg: string, ...args: any[]): void;
+  error(obj: object, msg?: string, ...args: any[]): void;
+  error(...args: Parameters<LoggerFn>) {
+    this.call("error", ...args);
+  }
+
+  fatal(msg: string, ...args: any[]): void;
+  fatal(obj: object, msg?: string, ...args: any[]): void;
+  fatal(...args: Parameters<LoggerFn>) {
+    this.call("fatal", ...args);
+  }
+
+  setContext(value: string) {
+    this.context = value;
+  }
+
+  private call(method: pino.Level, ...args: Parameters<LoggerFn>) {
+    const context = this.context;
+    if (context) {
+      const firstArg = args[0];
+      if (typeof firstArg === "object") {
+        args = [Object.assign({ context }, firstArg), ...args.slice(1)];
+      } else {
+        args = [{ context }, ...args];
+      }
+    }
+
+    (this.logger[method] as any)(...args);
+  }
+
+  private get logger() {
+    return getValue<pino.Logger>(LOGGER_KEY) || outOfContext;
+  }
+}
diff --git a/src/constants.ts b/src/constants.ts
new file mode 100644
index 0000000..0742550
--- /dev/null
+++ b/src/constants.ts
@@ -0,0 +1,3 @@
+export const LOGGER_KEY = "logger";
+
+export const OPTIONS_PROVIDER_TOKEN = "pino-options";
diff --git a/src/index.ts b/src/index.ts
index bc567b0..45ccaeb 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,191 +1,5 @@
-import {
-  Injectable,
-  LoggerService,
-  Global,
-  Module,
-  DynamicModule,
-  NestModule,
-  MiddlewareConsumer,
-  RequestMethod,
-  Inject
-} from "@nestjs/common";
-import { ModuleMetadata, Provider } from "@nestjs/common/interfaces";
-import * as express from "express";
-import * as pinoHttp from "pino-http";
-import * as pino from "pino";
-import { DestinationStream, LoggerOptions } from "pino";
-import { getValue, setValue, middleware as ctxMiddleware } from "express-ctx";
-
-type PassedLogger = { logger: pino.Logger };
-
-export type Params =
-  | null
-  | pinoHttp.Options
-  | DestinationStream
-  | [pinoHttp.Options, DestinationStream];
-
-export interface LoggerModuleAsyncOptions
-  extends Pick<ModuleMetadata, "imports" | "providers"> {
-  useFactory: (...args: any[]) => Params | Promise<Params>;
-  inject?: any[];
-}
-
-const LOGGER_KEY = "logger";
-
-const OPTIONS_PROVIDER_TOKEN = "pino-options";
-
-@Injectable()
-export class Logger implements LoggerService {
-  private readonly outOfContext: pino.Logger;
-
-  constructor(@Inject(OPTIONS_PROVIDER_TOKEN) options: Params) {
-    if (Array.isArray(options)) {
-      this.outOfContext = pino(...options);
-    } else if (isPassedLogger(options)) {
-      this.outOfContext = options.logger;
-    } else {
-      this.outOfContext = pino(options || undefined);
-    }
-  }
-
-  verbose(message: any, context?: string, ...args: any[]) {
-    if (context) {
-      this.logger.trace({ context }, message, ...args);
-    } else {
-      this.logger.trace(message);
-    }
-  }
-
-  debug(message: any, context?: string, ...args: any[]) {
-    if (context) {
-      this.logger.debug({ context }, message, ...args);
-    } else {
-      this.logger.debug(message);
-    }
-  }
-
-  log(message: any, context?: string, ...args: any[]) {
-    if (context) {
-      this.logger.info({ context }, message, ...args);
-    } else {
-      this.logger.info(message);
-    }
-  }
-
-  warn(message: any, context?: string, ...args: any[]) {
-    if (context) {
-      this.logger.warn({ context }, message, ...args);
-    } else {
-      this.logger.warn(message);
-    }
-  }
-
-  error(message: any, trace?: string, context?: string, ...args: any[]) {
-    if (context) {
-      this.logger.error({ context, trace }, message, ...args);
-    } else if (trace) {
-      this.logger.error({ trace }, message);
-    } else {
-      this.logger.error(message);
-    }
-  }
-
-  private get logger() {
-    return getValue<pino.Logger>(LOGGER_KEY) || this.outOfContext;
-  }
-}
-
-@Module({})
-export class LoggerModule {
-  static forRoot(
-    opts?: PassedLogger | LoggerOptions | DestinationStream
-  ): DynamicModule;
-  static forRoot(opts: LoggerOptions, stream: DestinationStream): DynamicModule;
-  static forRoot(
-    opts?: PassedLogger | LoggerOptions | DestinationStream,
-    stream?: DestinationStream
-  ): DynamicModule {
-    return {
-      module: LoggerModule,
-      imports: [
-        LoggerCoreModule.forRoot(
-          stream ? [opts as LoggerOptions, stream] : opts
-        )
-      ]
-    };
-  }
-
-  static forRootAsync(options: LoggerModuleAsyncOptions): DynamicModule {
-    return {
-      module: LoggerModule,
-      imports: [LoggerCoreModule.forRootAsync(options)]
-    };
-  }
-}
-
-@Global()
-@Module({ providers: [Logger], exports: [Logger] })
-class LoggerCoreModule implements NestModule {
-  static forRoot(options: Params | undefined): DynamicModule {
-    const optionsProvider: Provider<Params> = {
-      provide: OPTIONS_PROVIDER_TOKEN,
-      useValue: options || null
-    };
-
-    return {
-      module: LoggerCoreModule,
-      providers: [Logger, optionsProvider],
-      exports: [Logger]
-    };
-  }
-
-  static forRootAsync(options: LoggerModuleAsyncOptions): DynamicModule {
-    const optionsProvider: Provider<Params | Promise<Params>> = {
-      provide: OPTIONS_PROVIDER_TOKEN,
-      useFactory: options.useFactory,
-      inject: options.inject
-    };
-
-    return {
-      module: LoggerCoreModule,
-      imports: options.imports,
-      providers: options.providers
-        ? [Logger, optionsProvider, ...options.providers]
-        : [Logger, optionsProvider],
-      exports: [Logger]
-    };
-  }
-
-  constructor(
-    @Inject(OPTIONS_PROVIDER_TOKEN) private readonly options: Params
-  ) {}
-
-  configure(consumer: MiddlewareConsumer) {
-    consumer
-      .apply(...createLoggerMiddlewares(this.options))
-      .forRoutes({ path: "*", method: RequestMethod.ALL });
-  }
-}
-
-function isPassedLogger(
-  params: PassedLogger | LoggerOptions | DestinationStream | null
-): params is PassedLogger {
-  return !!params && "logger" in params;
-}
-
-function createLoggerMiddlewares(params: Params) {
-  const middleware = Array.isArray(params)
-    ? pinoHttp(...params)
-    : pinoHttp((params as pinoHttp.Options) || undefined);
-
-  return [ctxMiddleware, middleware, bindLoggerMiddleware];
-}
-
-function bindLoggerMiddleware(
-  req: express.Request,
-  res: express.Response,
-  next: express.NextFunction
-) {
-  setValue(LOGGER_KEY, req.log);
-  next();
-}
+export { LoggerModule } from "./LoggerModule";
+export { Params, LoggerModuleAsyncOptions } from "./params";
+export { Logger } from "./Logger";
+export { PinoLogger } from "./PinoLogger";
+export { InjectPinoLogger } from "./InjectPinoLogger";
diff --git a/src/params.ts b/src/params.ts
new file mode 100644
index 0000000..953a8a2
--- /dev/null
+++ b/src/params.ts
@@ -0,0 +1,24 @@
+import * as pinoHttp from "pino-http";
+import * as pino from "pino";
+import { DestinationStream } from "pino";
+import { ModuleMetadata } from "@nestjs/common/interfaces";
+
+export type PassedLogger = { logger: pino.Logger };
+
+export type Params =
+  | null
+  | pinoHttp.Options
+  | DestinationStream
+  | [pinoHttp.Options, DestinationStream];
+
+export interface LoggerModuleAsyncOptions
+  extends Pick<ModuleMetadata, "imports" | "providers"> {
+  useFactory: (...args: any[]) => Params | Promise<Params>;
+  inject?: any[];
+}
+
+export function isPassedLogger(
+  params: PassedLogger | pino.LoggerOptions | pino.DestinationStream | null
+): params is PassedLogger {
+  return !!params && "logger" in params;
+}


Test patch:
diff --git a/__tests__/inject.spec.ts b/__tests__/inject.spec.ts
new file mode 100644
index 0000000..425ba42
--- /dev/null
+++ b/__tests__/inject.spec.ts
@@ -0,0 +1,99 @@
+import { NestFactory } from "@nestjs/core";
+import { Module, Controller, Get, Injectable } from "@nestjs/common";
+import MemoryStream = require("memorystream");
+import * as request from "supertest";
+import { PinoLogger, InjectPinoLogger, LoggerModule } from "../src";
+import { platforms } from "./utils/platforms";
+import { fastifyExtraWait } from "./utils/fastifyExtraWait";
+import { parseLogs } from "./utils/logs";
+import { __resetOutOfContextForTests } from "../src/PinoLogger";
+
+describe("InjectPinoLogger", () => {
+  beforeEach(() => __resetOutOfContextForTests());
+
+  for (const PlatformAdapter of platforms) {
+    describe(PlatformAdapter.name, () => {
+      it("should work", async () => {
+        const stream = new MemoryStream();
+        const serviceLogMessage = Math.random().toString();
+        const controllerLogMessage = Math.random().toString();
+        const context = Math.random().toString();
+        let logs = "";
+
+        stream.on("data", (chunk: string) => {
+          logs += chunk.toString();
+        });
+
+        @Injectable()
+        class TestService {
+          constructor(
+            @InjectPinoLogger() private readonly logger: PinoLogger
+          ) {}
+          someMethod() {
+            this.logger.info(serviceLogMessage);
+          }
+        }
+
+        @Controller("/")
+        class TestController {
+          constructor(
+            private readonly service: TestService,
+            @InjectPinoLogger(context) private readonly logger: PinoLogger
+          ) {}
+          @Get()
+          get() {
+            this.logger.info({ foo: "bar" }, controllerLogMessage);
+            this.logger.info(controllerLogMessage);
+            this.service.someMethod();
+            return {};
+          }
+        }
+
+        @Module({
+          imports: [LoggerModule.forRoot(stream)],
+          controllers: [TestController],
+          providers: [TestService]
+        })
+        class TestModule {}
+
+        const app = await NestFactory.create(
+          TestModule,
+          new PlatformAdapter(),
+          { logger: false }
+        );
+        const server = app.getHttpServer();
+
+        await app.init();
+        await fastifyExtraWait(PlatformAdapter, app);
+
+        await request(server).get("/");
+
+        await app.close();
+
+        const parsedLogs = parseLogs(logs);
+
+        const serviceLogObject = parsedLogs.find(
+          v => v.msg === serviceLogMessage && v.req && !v.context
+        );
+        expect(serviceLogObject).toBeTruthy();
+
+        const controllerLogObject1 = parsedLogs.find(
+          v =>
+            v.msg === controllerLogMessage &&
+            v.req &&
+            v.context === context &&
+            (v as any).foo === "bar"
+        );
+        const controllerLogObject2 = parsedLogs.find(
+          v =>
+            v.msg === controllerLogMessage &&
+            v.req &&
+            v.context === context &&
+            !("foo" in v)
+        );
+        expect(controllerLogObject1).toBeTruthy();
+        expect(controllerLogObject2).toBeTruthy();
+      });
+    });
+  }
+});
diff --git a/__tests__/levels.spec.ts b/__tests__/levels.spec.ts
index a79158a..26b990c 100644
--- a/__tests__/levels.spec.ts
+++ b/__tests__/levels.spec.ts
@@ -7,8 +7,9 @@ import { Logger, LoggerModule } from "../src";
 import { platforms } from "./utils/platforms";
 import { fastifyExtraWait } from "./utils/fastifyExtraWait";
 import { parseLogs } from "./utils/logs";
+import { __resetOutOfContextForTests, PinoLogger } from "../src/PinoLogger";
 
-const methods: [keyof Logger, pino.Level][] = [
+const loggerMethods: [keyof Logger, pino.Level][] = [
   ["verbose", "trace"],
   ["debug", "debug"],
   ["log", "info"],
@@ -16,10 +17,16 @@ const methods: [keyof Logger, pino.Level][] = [
   ["error", "error"]
 ];
 
-describe("levels", () => {
+const pinoLoggerMethods: pino.Level[] = loggerMethods
+  .map(p => p[1])
+  .concat("fatal");
+
+describe(`${Logger.name} levels`, () => {
+  beforeEach(() => __resetOutOfContextForTests());
+
   for (const PlatformAdapter of platforms) {
     describe(PlatformAdapter.name, () => {
-      for (const [loggerMethodName, pinoLevel] of methods) {
+      for (const [loggerMethodName, pinoLevel] of loggerMethods) {
         it(loggerMethodName, async () => {
           const stream = new MemoryStream();
           const serviceLogMessage = Math.random().toString();
@@ -110,21 +117,102 @@ describe("levels", () => {
           const responseLogObject = parsedLogs.find(
             log => log.msg === "request completed"
           );
-          expect(responseLogObject)[
-            pinoLevel === "warn" || pinoLevel === "error"
-              ? "toBeFalsy"
-              : "toBeTruthy"
-          ]();
-
-          // Because of nest itself logs has level "info"
-          // logs of it are not always exists
+
+          // Because of nest itself logs and response logs has level "info"
+          // they are not always exists
           const appLogObject = parsedLogs.find(log =>
             log.msg.startsWith(appLogMessage)
           );
           if (pinoLevel === "warn" || pinoLevel === "error") {
             expect(appLogObject).toBeFalsy();
+            expect(responseLogObject).toBeFalsy();
           } else {
             expect(appLogObject).toBeTruthy();
+            expect(responseLogObject).toBeTruthy();
+          }
+        });
+      }
+    });
+  }
+});
+
+describe(`${PinoLogger.name} levels`, () => {
+  beforeEach(() => __resetOutOfContextForTests());
+
+  for (const PlatformAdapter of platforms) {
+    describe(PlatformAdapter.name, () => {
+      // add fatal method
+      for (const pinoLevel of pinoLoggerMethods) {
+        it(pinoLevel, async () => {
+          const stream = new MemoryStream();
+          const serviceLogMessage = Math.random().toString();
+          let logs = "";
+          const trace = "trace";
+
+          stream.on("data", (chunk: string) => {
+            logs += chunk.toString();
+          });
+
+          @Injectable()
+          class TestService {
+            constructor(private readonly logger: PinoLogger) {}
+            someMethod() {
+              this.logger[pinoLevel](serviceLogMessage);
+            }
+          }
+
+          @Controller("/")
+          class TestController {
+            constructor(private readonly service: TestService) {}
+            @Get()
+            get() {
+              this.service.someMethod();
+              return {};
+            }
+          }
+
+          @Module({
+            imports: [LoggerModule.forRoot({ level: pinoLevel }, stream)],
+            controllers: [TestController],
+            providers: [TestService]
+          })
+          class TestModule {}
+
+          const app = await NestFactory.create(
+            TestModule,
+            new PlatformAdapter(),
+            { logger: false }
+          );
+          const server = app.getHttpServer();
+
+          await app.init();
+          await fastifyExtraWait(PlatformAdapter, app);
+
+          await request(server).get("/");
+
+          await app.close();
+
+          const parsedLogs = parseLogs(logs);
+
+          const serviceLogObject = parsedLogs.find(
+            v => v.msg === serviceLogMessage
+          );
+          expect(serviceLogObject).toBeTruthy();
+
+          const responseLogObject = parsedLogs.find(
+            log => log.msg === "request completed"
+          );
+
+          // Because of response logs has level "info"
+          // they are not always exists
+          if (
+            pinoLevel === "warn" ||
+            pinoLevel === "error" ||
+            pinoLevel === "fatal"
+          ) {
+            expect(responseLogObject).toBeFalsy();
+          } else {
+            expect(responseLogObject).toBeTruthy();
           }
         });
       }
diff --git a/__tests__/module.spec.ts b/__tests__/module.spec.ts
index 4e30bec..ae959e2 100644
--- a/__tests__/module.spec.ts
+++ b/__tests__/module.spec.ts
@@ -6,8 +6,11 @@ import { Logger, LoggerModule } from "../src";
 import { platforms } from "./utils/platforms";
 import { fastifyExtraWait } from "./utils/fastifyExtraWait";
 import { parseLogs } from "./utils/logs";
+import { __resetOutOfContextForTests } from "../src/PinoLogger";
 
 describe("module initialization", () => {
+  beforeEach(() => __resetOutOfContextForTests());
+
   for (const PlatformAdapter of platforms) {
     describe(PlatformAdapter.name, () => {
       describe("forRoot", () => {
diff --git a/__tests__/pass-existing.spec.ts b/__tests__/pass-existing.spec.ts
index 6f6aefd..7b942b1 100644
--- a/__tests__/pass-existing.spec.ts
+++ b/__tests__/pass-existing.spec.ts
@@ -7,8 +7,11 @@ import { Logger, LoggerModule } from "../src";
 import { platforms } from "./utils/platforms";
 import { fastifyExtraWait } from "./utils/fastifyExtraWait";
 import { parseLogs } from "./utils/logs";
+import { __resetOutOfContextForTests } from "../src/PinoLogger";
 
 describe("pass existing logger", () => {
+  beforeEach(() => __resetOutOfContextForTests());
+
   for (const PlatformAdapter of platforms) {
     describe(PlatformAdapter.name, () => {
       it("should be used by app and by service", async () => {