Repository: iamolegga/nestjs-pino (ts)

Issue:
[BUG] Missing stack trace of thrown error in log produced by nestjs built-in error handler
<!-- Please don't delete this template or we'll close your issue -->
<!-- Before creating an issue please make sure you are using the latest version. -->

[x] I've read [the docs of nestjs-pino](https://github.com/iamolegga/nestjs-pino/blob/master/README.md)

[x] I've read [the docs of pino](https://getpino.io/#/)

[x] I couldn't find the same [open issue of nestjs-pino](https://github.com/iamolegga/nestjs-pino/issues)

There is an open issue https://github.com/iamolegga/nestjs-pino/issues/535, but that issue focuses on `express`, but I am not convinced my issue is related to `express`.

**What is the current behavior?**
`nestjs-pino@2.3.0` is missing stack traces

**What is the expected behavior?**
Stack traces should be included in error logs, just like previously in `nestjs-pino@1.4.0`

**Please provide minimal example repo. Without it this issue will be closed**
I created two NestJS applications, older one includes stack traces in error logs, newer version does not.

See examples here: https://github.com/raidokaldma/nestjs-pino-issue



Pull request:
log stack from call of built-in error handlers
fix #656 

Patch:
diff --git a/src/Logger.ts b/src/Logger.ts
index 201e7a3..d9d8927 100644
--- a/src/Logger.ts
+++ b/src/Logger.ts
@@ -52,8 +52,44 @@ export class Logger implements LoggerService {
         Object.assign(objArg, message);
       }
       this.logger[level](objArg, ...params);
+    } else if (this.isWrongExceptionsHandlerContract(level, message, params)) {
+      objArg.err = new Error(message);
+      objArg.err.stack = params[0];
+      this.logger[level](objArg);
     } else {
       this.logger[level](objArg, message, ...params);
     }
   }
+
+  /**
+   * Unfortunately built-in (not only) `^.*Exception(s?)Handler$` classes call `.error`
+   * method with not supported contract:
+   *
+   * - ExceptionsHandler
+   * @see https://github.com/nestjs/nest/blob/35baf7a077bb972469097c5fea2f184b7babadfc/packages/core/exceptions/base-exception-filter.ts#L60-L63
+   *
+   * - ExceptionHandler
+   * @see https://github.com/nestjs/nest/blob/99ee3fd99341bcddfa408d1604050a9571b19bc9/packages/core/errors/exception-handler.ts#L9
+   *
+   * - WsExceptionsHandler
+   * @see https://github.com/nestjs/nest/blob/9d0551ff25c5085703bcebfa7ff3b6952869e794/packages/websockets/exceptions/base-ws-exception-filter.ts#L47-L50
+   *
+   * - RpcExceptionsHandler @see https://github.com/nestjs/nest/blob/9d0551ff25c5085703bcebfa7ff3b6952869e794/packages/microservices/exceptions/base-rpc-exception-filter.ts#L26-L30
+   *
+   * - all of them
+   * @see https://github.com/search?l=TypeScript&q=org%3Anestjs+logger+error+stack&type=Code
+   */
+  private isWrongExceptionsHandlerContract(
+    level: Level,
+    message: any,
+    params: any[],
+  ): params is [string] {
+    return (
+      level === 'error' &&
+      typeof message === 'string' &&
+      params.length === 1 &&
+      typeof params[0] === 'string' &&
+      /\n\s*at /.test(params[0])
+    );
+  }
 }


Test patch:
diff --git a/__tests__/errors.spec.ts b/__tests__/errors.spec.ts
index 316467c..641d292 100644
--- a/__tests__/errors.spec.ts
+++ b/__tests__/errors.spec.ts
@@ -172,6 +172,41 @@ describe('error logging', () => {
           ).toBeTruthy();
         });
       });
+
+      describe('keeps stack of thrown error', () => {
+        it('built-in error handler logs with correct stack', async () => {
+          const msg = Math.random().toString();
+
+          @Controller('/')
+          class TestController {
+            @Get()
+            get() {
+              throw new Error(msg);
+            }
+          }
+
+          const logs = await new TestCase(new PlatformAdapter(), {
+            controllers: [TestController],
+          })
+            .forRoot()
+            .expectError(500)
+            .run();
+
+          expect(
+            logs.some(
+              (v) =>
+                v.req &&
+                v.msg === msg &&
+                v.err &&
+                v.err.message === msg &&
+                v.err.stack.includes(__filename) &&
+                v.err.stack.includes(
+                  `${TestController.name}.${TestController.prototype.get.name}`,
+                ),
+            ),
+          ).toBeTruthy();
+        });
+      });
     });
   }
 });
diff --git a/__tests__/utils/logs.ts b/__tests__/utils/logs.ts
index 6cb246a..e1e35c1 100644
--- a/__tests__/utils/logs.ts
+++ b/__tests__/utils/logs.ts
@@ -6,7 +6,7 @@ export type LogObject = {
   req?: { id: number };
   res?: Record<string, any>;
   context?: string;
-  err?: any;
+  err?: { message: string; stack: string };
   [key: string]: any;
 };
 
diff --git a/__tests__/utils/test-case.ts b/__tests__/utils/test-case.ts
index f3ebf53..dee1a9e 100644
--- a/__tests__/utils/test-case.ts
+++ b/__tests__/utils/test-case.ts
@@ -17,6 +17,7 @@ import { LogsContainer } from './logs';
 export class TestCase {
   private module?: Type<any>;
   private stream: pino.DestinationStream;
+  private expectedCode = 200;
 
   constructor(
     private readonly adapter: AbstractHttpAdapter<any, any, any>,
@@ -74,6 +75,11 @@ export class TestCase {
     return this;
   }
 
+  expectError(code: number): this {
+    this.expectedCode = code;
+    return this;
+  }
+
   async run(...paths: string[]): Promise<LogsContainer> {
     if (paths.length === 0) {
       paths = ['/'];
@@ -89,7 +95,7 @@ export class TestCase {
 
     const server = await app.listen(3000);
     for (const path of paths) {
-      await request(server).get(path).expect(200);
+      await request(server).get(path).expect(this.expectedCode);
     }
     await app.close();