Repository: iamolegga/nestjs-pino (ts)

Issue:
[FEATURE REQUEST] Add support for pino-http's customAttributeKeys.err option
**Is your feature request related to a problem? Please describe.**
`pino` allows setting the [`errorKey`](https://github.com/pinojs/pino/blob/master/docs/api.md#errorkey-string), the "string key for the 'error' in the JSON object" and `pino-http` allows customizing the `err` log object attribute via [`opts.customAttributeKeys.key`](https://github.com/pinojs/pino-http#pinohttpopts-stream), but `nestjs-pino` is hardcoded to use `err` in the `PinoLogger`: https://github.com/iamolegga/nestjs-pino/blob/3.4.0/src/PinoLogger.ts#L138

We use Datadog and it requires that errors are in the `error` attribute of the log object, effectively ignoring `err`.

**Describe the solution you'd like**
Consider using the `pinoHttp.customAttributeKeys.err` option when passing the error into the merge object field.

Happy to put up a pull request if you'd like.

**Describe alternatives you've considered**
I can override the `call` method of the `PinoLogger` in a derived class to account for the `pinoHttp.customAttributeKeys.key` option, this is my current solution, but I've love to see this implemented natively in `nestjs-pino`.

Something like this: 
```
  protected readonly errorKey = 'err';

  constructor(
    @Inject(PARAMS_PROVIDER_TOKEN) { pinoHttp, renameContext }: Params,
  ) {
    // ...

    if (
      typeof pinoHttp === 'object' &&
      'customAttributeKeys' in pinoHttp &&
      typeof pinoHttp.customAttributeKeys !== 'undefined'
    ) {
      this.errorKey = pinoHttp.customAttributeKeys.err ?? 'err';
    }
  }
```


```
  protected call(method: pino.Level, ...args: Parameters<LoggerFn>) {
    //...
        if (firstArg instanceof Error) {
          args = [
            Object.assign(
              { [this.contextName]: this.context },
              { [this.errorKey]: firstArg },
            ),
            ...args.slice(1),
          ];
        }
    //...
  }
```



Pull request description:
add custom err attribute key support
closes #1662 

Patch:
diff --git a/package-lock.json b/package-lock.json
index aba838e..235bb60 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -2089,9 +2089,9 @@
       "dev": true
     },
     "node_modules/@types/node": {
-      "version": "20.6.2",
-      "resolved": "https://registry.npmjs.org/@types/node/-/node-20.6.2.tgz",
-      "integrity": "sha512-Y+/1vGBHV/cYk6OI1Na/LHzwnlNCAfU3ZNGrc1LdRe/LAIbdDPTTv/HU3M7yXN448aTVDq3eKRm2cg7iKLb8gw==",
+      "version": "20.6.3",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-20.6.3.tgz",
+      "integrity": "sha512-HksnYH4Ljr4VQgEy2lTStbCKv/P590tmPe5HqOnv9Gprffgv5WXAY+Y5Gqniu0GGqeTCUdBnzC3QSrzPkBkAMA==",
       "dev": true
     },
     "node_modules/@types/qs": {
@@ -11103,9 +11103,9 @@
       "dev": true
     },
     "@types/node": {
-      "version": "20.6.2",
-      "resolved": "https://registry.npmjs.org/@types/node/-/node-20.6.2.tgz",
-      "integrity": "sha512-Y+/1vGBHV/cYk6OI1Na/LHzwnlNCAfU3ZNGrc1LdRe/LAIbdDPTTv/HU3M7yXN448aTVDq3eKRm2cg7iKLb8gw==",
+      "version": "20.6.3",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-20.6.3.tgz",
+      "integrity": "sha512-HksnYH4Ljr4VQgEy2lTStbCKv/P590tmPe5HqOnv9Gprffgv5WXAY+Y5Gqniu0GGqeTCUdBnzC3QSrzPkBkAMA==",
       "dev": true
     },
     "@types/qs": {
diff --git a/src/PinoLogger.ts b/src/PinoLogger.ts
index c7d46d8..d6461c0 100644
--- a/src/PinoLogger.ts
+++ b/src/PinoLogger.ts
@@ -48,10 +48,19 @@ export class PinoLogger implements PinoMethods {
 
   protected context = '';
   protected readonly contextName: string;
+  protected readonly errorKey: string = 'err';
 
   constructor(
     @Inject(PARAMS_PROVIDER_TOKEN) { pinoHttp, renameContext }: Params,
   ) {
+    if (
+      typeof pinoHttp === 'object' &&
+      'customAttributeKeys' in pinoHttp &&
+      typeof pinoHttp.customAttributeKeys !== 'undefined'
+    ) {
+      this.errorKey = pinoHttp.customAttributeKeys.err ?? 'err';
+    }
+
     if (!outOfContext) {
       if (Array.isArray(pinoHttp)) {
         outOfContext = pino(...pinoHttp);
@@ -135,7 +144,7 @@ export class PinoLogger implements PinoMethods {
           args = [
             Object.assign(
               { [this.contextName]: this.context },
-              { err: firstArg },
+              { [this.errorKey]: firstArg },
             ),
             ...args.slice(1),
           ];


Test patch:
diff --git a/__tests__/errors.spec.ts b/__tests__/errors.spec.ts
index 3b16d00..56a723f 100644
--- a/__tests__/errors.spec.ts
+++ b/__tests__/errors.spec.ts
@@ -175,6 +175,76 @@ describe('error logging', () => {
         });
       });
 
+      describe('setting custom attribute keys', () => {
+        it('setting the `err` custom attribute key', async () => {
+          const ctx = Math.random().toString();
+          const message = 'custom `err` attribute key';
+
+          @Controller('/')
+          class TestController {
+            constructor(private readonly logger: PinoLogger) {
+              this.logger.setContext(ctx);
+            }
+
+            @Get()
+            get() {
+              this.logger.info(new Error(message), 'baz');
+              return {};
+            }
+          }
+
+          const logs = await new TestCase(new PlatformAdapter(), {
+            controllers: [TestController],
+          })
+            .forRoot({ pinoHttp: { customAttributeKeys: { err: 'error' } } })
+            .run();
+          expect(
+            logs.some(
+              (v) =>
+                v.req &&
+                v.context === ctx &&
+                !v.err &&
+                v.error &&
+                (v.error as { message: string }).message === message,
+            ),
+          ).toBeTruthy();
+        });
+
+        it('setting the `req` custom attribute key', async () => {
+          const ctx = Math.random().toString();
+          const message = 'custom `req` attribute key';
+
+          @Controller('/')
+          class TestController {
+            constructor(private readonly logger: PinoLogger) {
+              this.logger.setContext(ctx);
+            }
+
+            @Get()
+            get() {
+              this.logger.info(new Error(message), 'baz');
+              return {};
+            }
+          }
+
+          const logs = await new TestCase(new PlatformAdapter(), {
+            controllers: [TestController],
+          })
+            .forRoot({ pinoHttp: { customAttributeKeys: { req: 'request' } } })
+            .run();
+          expect(
+            logs.some(
+              (v) =>
+                !v.req &&
+                v.request &&
+                v.context === ctx &&
+                v.err &&
+                v.err.message === message,
+            ),
+          ).toBeTruthy();
+        });
+      });
+
       describe('keeps stack of thrown error', () => {
         it('built-in error handler logs with correct stack', async () => {
           const msg = Math.random().toString();