7

I have a global logger module in nest, that logs to a cloud logging service. I am trying to create a class method decorator that adds logging functionality. But I am struggling how to inject the service of a global nest module inside a decorator, since all dependency injection mechanisms I found in the docs depend are class or class property based injection.

export function logDecorator() {

  // I would like to inject a LoggerService that is a provider of a global logger module
  let logger = ???

  return (target: any, propertyKey: string, propertyDescriptor: PropertyDescriptor) => {
    //get original method
    const originalMethod = propertyDescriptor.value;

    //redefine descriptor value within own function block
    propertyDescriptor.value = function(...args: any[]) {
      logger.log(`${propertyKey} method called with args.`);
      
      //attach original method implementation
      const result = originalMethod.apply(this, args);

      //log result of method
      logger.log(`${propertyKey} method return value`);
    };
  };
}

UPDATE: Per reqest a simple example Basic example would be to log calls to a service method using my custom logger (which in my case logs to a cloud service):

class MyService {
    @logDecorator()
    someMethod(name: string) {
        // calls to this method as well as method return values would be logged to CloudWatch
        return `Hello ${name}`
    }
}

Another extended use case would be to catch some errors, then log them. I have a lot of this kind of logic that get reused across all my services.

mrzli
  • 16,799
  • 4
  • 38
  • 45
florian norbert bepunkt
  • 2,099
  • 1
  • 21
  • 32

1 Answers1

29

Okay, found a solution. In case anyone else stumbles upon this. First please keep in mind how decorators work – they are class constructor based, not instance based.

In my case I wanted to have my logger service injected in the class instance. So the solution is to tell Nest in the decorator to inject the LoggerService into the instance of the class that contains the decorated method.

import { Inject } from '@nestjs/common';
import { LoggerService } from '../../logger/logger.service';

export function logErrorDecorator(bubble = true) {
  const injectLogger = Inject(LoggerService);

  return (target: any, propertyKey: string, propertyDescriptor: PropertyDescriptor) => {
    injectLogger(target, 'logger'); // this is the same as using constructor(private readonly logger: LoggerService) in a class

    //get original method
    const originalMethod = propertyDescriptor.value;

    //redefine descriptor value within own function block
    propertyDescriptor.value = async function(...args: any[]) {
      try {
        return await originalMethod.apply(this, args);
      } catch (error) {
        const logger: LoggerService = this.logger;

        logger.setContext(target.constructor.name);
        logger.error(error.message, error.stack);

        // rethrow error, so it can bubble up
        if (bubble) {
          throw error;
        }
      }
    };
  };
}

This gives the possibility to catch errors in a method, log them within the service context, and either re-throw them (so your controllers can handle user resp) or not. In my case I also had to implement some transaction-related logic here.

export class FoobarService implements OnModuleInit {
  onModuleInit() {
    this.test();
  }

  @logErrorDecorator()
  test() {
    throw new Error('Oh my');
  }
}
mrzli
  • 16,799
  • 4
  • 38
  • 45
florian norbert bepunkt
  • 2,099
  • 1
  • 21
  • 32
  • 4
    When I use this approach the service ends up being undefined in the descriptions value function. In my case the service I’m using also depends on two other services, so that may have an impact. Can this solution be used for services that inject other ones? – vinnymac Mar 27 '21 at 14:05
  • @vinnymac Oi, have you found out what was causing your service to come undefined? Seems like I'm having the same problem here. Even though it works nicely when I use this decorator in a service class, the injected service turns undefined when I use it in a different class, in a factory, to be precise. I tried making it injectable but it didn't help. – Albert Apr 23 '21 at 19:13
  • 2
    Hey @Albert, Have you found out what was causing your service to be undefined? – hikvineh May 29 '21 at 13:52
  • @hikvineh Hi, yes I have but I don't remember already what it was exactly but it was just some minor thing, after I fixed it, everything worked as expected, so yeah I confirm that the solution described in the answer works! And this service I was injecting into my decorator also injects a bunch of other dependencies, so it does not really matter if your service has other services injected into it, everything works as expected. – Albert May 29 '21 at 22:15
  • My guess that it could be circular dependency problem. – valerii15298 Apr 18 '22 at 06:38
  • This is awesome, thank you. I'm trying to use this for a logger as well and my problem is some providers have the logger as part of the constructor and some don't. Would it be possible to do the injection conditionally? – sshevlyagin Apr 21 '22 at 02:44
  • @valerii15298 it very well may have been a circular dependency issue. I will have to look back at my notes and see what I did. If I recall, I ended up foregoing this solution entirely as I couldn't get it working, but your theory makes sense as the cause to me. – vinnymac May 06 '22 at 18:19
  • When I use this approach, compiler complains that logger does not exist, how did you overcome this? – Eddy Jun 28 '22 at 10:02
  • anyone figured out this issue with undefined service? – Goran Jakovljevic Oct 23 '22 at 13:09
  • @GoranJakovljevic my service was undefined because I forgot to return `propertyDescriptor` after I redefined `propertyDescriptor.value` – harrolee Mar 09 '23 at 18:13