Repository: iamolegga/nestjs-pino (ts)

Issue:
[FEATURE REQUEST] Changing loglevel during runtime.
Is there any way to dynamically change `loglevel `during runtime? Like in pino using `logger.level`.

https://github.com/pinojs/pino/blob/master/docs/api.md#loggerlevel-string-gettersetter

It will be very useful in my case, when I want to change `loglevel `in working microservice without its reloading. Now I set it with `.env` and there is no way to change it dynamicly. 

Pull request description:
runtime update
fix #371 

Patch:
diff --git a/README.md b/README.md
index 3ac5a86..c0500bb 100644
--- a/README.md
+++ b/README.md
@@ -406,6 +406,21 @@ class MyService {
 }
 ```
 
+## Change pino params at runtime
+
+Pino root instance with passed via module registration params creates a separate child logger for every request. This root logger params can be changed at runtime via `PinoLogger.root` property which is the pointer to logger instance. Example:
+
+```ts
+@Controller('/')
+class TestController {
+  @Post('/change-loggin-level')
+  setLevel() {
+    PinoLogger.root.level = 'info';
+    return null;
+  }
+}
+```
+
 ## Migration
 
 ### v1
diff --git a/src/LoggerModule.ts b/src/LoggerModule.ts
index add2a1d..8f6236d 100644
--- a/src/LoggerModule.ts
+++ b/src/LoggerModule.ts
@@ -97,12 +97,21 @@ function createLoggerMiddlewares(
   }
 
   if (Array.isArray(params)) {
-    return [pinoHttp(...params), bindLoggerMiddleware];
+    const middleware = pinoHttp(...params);
+    // @ts-expect-error: root is readonly field, but this is the place where
+    // it's set actually
+    PinoLogger.root = middleware.logger;
+    return [middleware, bindLoggerMiddleware];
   }
 
+  const middleware = pinoHttp(params as any);
+  // @ts-expect-error: root is readonly field, but this is the place where
+  // it's set actually
+  PinoLogger.root = middleware.logger;
+
   // FIXME: params type here is pinoHttp.Options | pino.DestinationStream
   // pinoHttp has two overloads, each of them takes those types
-  return [pinoHttp(params as any), bindLoggerMiddleware];
+  return [middleware, bindLoggerMiddleware];
 }
 
 function bindLoggerMiddleware(
diff --git a/src/PinoLogger.ts b/src/PinoLogger.ts
index a565392..f4622bd 100644
--- a/src/PinoLogger.ts
+++ b/src/PinoLogger.ts
@@ -30,13 +30,21 @@ let outOfContext: pino.Logger | undefined;
 
 export function __resetOutOfContextForTests() {
   outOfContext = undefined;
+  // @ts-ignore reset root for tests only
+  PinoLogger.root = undefined;
 }
 
 @Injectable({ scope: Scope.TRANSIENT })
 export class PinoLogger implements PinoMethods {
+  /**
+   * root is the most root logger that can be used to change params at runtime.
+   * Accessible only when `useExisting` is not set to `true` in `Params`.
+   * Readonly, but you can change it's properties.
+   */
+  static readonly root: pino.Logger;
+
   private context = '';
   private readonly contextName: string;
-  private readonly opts: any;
 
   constructor(
     @Inject(PARAMS_PROVIDER_TOKEN) { pinoHttp, renameContext }: Params,
@@ -58,7 +66,6 @@ export class PinoLogger implements PinoMethods {
     }
 
     this.contextName = renameContext || 'context';
-    this.opts = pinoHttp;
   }
 
   trace(msg: string, ...args: any[]): void;


Test patch:
diff --git a/__tests__/runtime-update.spec.ts b/__tests__/runtime-update.spec.ts
new file mode 100644
index 0000000..dc6903e
--- /dev/null
+++ b/__tests__/runtime-update.spec.ts
@@ -0,0 +1,64 @@
+import { Controller, Get } from '@nestjs/common';
+import { FastifyAdapter } from '@nestjs/platform-fastify';
+import { PinoLogger } from '../src';
+import { platforms } from './utils/platforms';
+import { TestCase } from './utils/test-case';
+
+describe('runtime update', () => {
+  for (const PlatformAdapter of platforms) {
+    describe(PlatformAdapter.name, () => {
+      it('works', async () => {
+        const msg1 = Math.random().toString();
+        const msg2 = Math.random().toString();
+        const messages = [msg1, msg2];
+        let msgIdx = 0;
+
+        const pathCheck = '/log-with-level-info';
+        const pathSetLevel = '/set-level-info';
+
+        @Controller('/')
+        class TestController {
+          constructor(private readonly logger: PinoLogger) {}
+
+          @Get(pathCheck)
+          check() {
+            this.logger.info(messages[msgIdx++]);
+            return {};
+          }
+
+          @Get(pathSetLevel)
+          setLevel() {
+            PinoLogger.root.level = 'info';
+            return {};
+          }
+        }
+
+        const logs = await new TestCase(new PlatformAdapter(), {
+          controllers: [TestController],
+        })
+          .forRoot({ pinoHttp: { level: 'silent' } })
+          .run(pathCheck, pathSetLevel, pathCheck);
+
+        expect(logs.some((l) => l.msg === msg1)).toBeFalsy();
+        expect(logs.some((l) => l.msg === msg2)).toBeTruthy();
+      });
+    });
+  }
+
+  it("doesn't work with useExisting", async () => {
+    @Controller('/')
+    class TestController {
+      @Get()
+      setLevel() {
+        expect(PinoLogger.root).toBeUndefined();
+        return {};
+      }
+    }
+
+    await new TestCase(new FastifyAdapter(), {
+      controllers: [TestController],
+    })
+      .forRoot({ useExisting: true })
+      .run();
+  });
+});
diff --git a/__tests__/utils/test-case.ts b/__tests__/utils/test-case.ts
index 817cf5c..f3ebf53 100644
--- a/__tests__/utils/test-case.ts
+++ b/__tests__/utils/test-case.ts
@@ -11,7 +11,7 @@ import {
   LoggerModuleAsyncParams,
   Params,
 } from '../../src';
-import { __resetOutOfContextForTests } from '../../src/PinoLogger';
+import { __resetOutOfContextForTests as __resetSingletons } from '../../src/PinoLogger';
 import { LogsContainer } from './logs';
 
 export class TestCase {
@@ -74,10 +74,13 @@ export class TestCase {
     return this;
   }
 
-  async run(path = '/'): Promise<LogsContainer> {
+  async run(...paths: string[]): Promise<LogsContainer> {
+    if (paths.length === 0) {
+      paths = ['/'];
+    }
     expect(this.module).toBeTruthy();
 
-    __resetOutOfContextForTests();
+    __resetSingletons();
 
     const app = await NestFactory.create(this.module, this.adapter, {
       bufferLogs: true,
@@ -85,7 +88,9 @@ export class TestCase {
     app.useLogger(app.get(Logger));
 
     const server = await app.listen(3000);
-    await request(server).get(path).expect(200);
+    for (const path of paths) {
+      await request(server).get(path).expect(200);
+    }
     await app.close();
 
     return LogsContainer.from(this.stream);