Lessons learned integrating AppSignal APM with NestJS
Tags: node, javascript, nestjs, apm
Published: 2022-05-20 13:05:53

At Superface, we use NestJS framework for backend, and AppSignal APM for monitoring and error tracking. While AppSignal provides a Node.js integration, getting it up and running with NestJS proved to be somewhat tricky.

In this blog post, I will share how we managed to get AppSignal to work with NestJS.

Code snippets used in this blog post are part of our [example project](https://github.com/superfaceai/nestjs-appsignal-example).

## AppSignal initialization and configuration

AppSignal uses auto-instrumentation which attaches hooks into Node.js tools and frameworks (Express, Koa, PostgreSQL, Redis, …) and observes for certain functions to be called. Once the functions are called, the instrumentation automatically collects trace spans on behalf of your application.

AppSignal has the following requirements (taken from AppSignal [docs](https://docs.appsignal.com/nodejs/installation.html)) to make auto-instrumentation work:

> To auto-instrument modules, the `Appsignal` module must be both **required** and **initialized** before any other package.

The standard way to instantiate objects in NestJS is using the Dependency Injection (DI) Container.

To fulfill the requirement, we cannot use NestJS DI Container to instantiate AppSignal. AppSignal has to be instantiated as a global variable, which also means that we cannot take advantage of NestJS `ConfigModule`.

Example of AppSignal instantiation and configuration using environment variables:

```ts
//source file: src/appsignal.ts

const name = process.env.APPSIGNAL_NAME;
const pushApiKey = process.env.APPSIGNAL_PUSH_API_KEY;
const active =
  process.env.APPSIGNAL_ACTIVE === '1' ||
  process.env.APPSIGNAL_ACTIVE === 'true';

export const appsignal = new Appsignal({
  active,
  name,
  pushApiKey,
});
```

[source code](https://github.com/superfaceai/nestjs-appsignal-example/blob/4432a20e8452f08e5edaecb9e54b4d389f286847/src/appsignal.ts)

You also need to register the AppSignal middleware when initializing Express in NestJS application bootstrap code:

```ts
//source file: src/main.ts

async function bootstrap() {
  const app = await NestFactory.create<NestExpressApplication>(AppModule);

  app.use(appsignalExpressMiddleware(appsignal));

  await app.listen(3000);
}
bootstrap();
```

[source code](https://github.com/superfaceai/nestjs-appsignal-example/blob/4432a20e8452f08e5edaecb9e54b4d389f286847/src/main.ts)

That's it, once you set `APPSIGNAL_PUSH_API_KEY` environment variable to valid AppSignal key and configure `APPSIGNAL_NAME`, `APPSIGNAL_ACTIVE` environment variables, AppSignal will start collecting metrics from all HTTP requests handled by your application.


![AppSignal Dashboard](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9v30t8cnh807eoopmxw7.jpg)

## Error tracking

Nest comes with a built-in exceptions layer, which is responsible for processing all unhandled exceptions across an application. See Nest [Exception filters docs](https://docs.nestjs.com/exception-filters) for details.

To track errors handled by Nest exception filters, we have created `AppsignalExceptionFilter` which implements Nest `ExceptionFilter` interface.

```ts
//source file: src/exception_filters/appsignal_exception.filter.ts

@Catch()
export class AppsignalExceptionFilter<T extends Error>
  implements ExceptionFilter
{
  catch(error: T, _host: ArgumentsHost) {
    let status: number;
    const tracer = appsignal.tracer();

    if (!tracer) {
      return;
    }

    if (error instanceof HttpException) {
      status = error.getStatus();
    }

    if (error && (!status || (status && status >= 500))) {
      tracer.setError(error);
    }
  }
}
```

[source code](https://github.com/superfaceai/nestjs-appsignal-example/blob/4432a20e8452f08e5edaecb9e54b4d389f286847/src/exception_filters/appsignal_exception.filter.ts)

The `AppsignalExceptionFilter` tracks `HttpException` exceptions with status code 5xx and any other exception types.

You can use `AppsignalExceptionFilter` by extending it in your custom exception filter implementation and register your exception filter in Nest app.

Example of extending `AppsignalExceptionFilter`:

```ts
//source file: src/exception_filters/all_exception.filter.ts

@Catch()
export class AllExceptionFilter extends AppsignalExceptionFilter<Error> {
  catch(error: Error, host: ArgumentsHost) {
    super.catch(error, host);

    const ctx = host.switchToHttp();
    const req = ctx.getRequest<Request>();
    const res = ctx.getResponse<Response>();

    const status = 500;

    const problem = {
      status,
      title: 'Internal server error',
      instance: req.path,
    };

    res.status(status).contentType('application/problem+json').json(problem);
  }
}
```

[source code](https://github.com/superfaceai/nestjs-appsignal-example/blob/4432a20e8452f08e5edaecb9e54b4d389f286847/src/exception_filters/all_exception.filter.ts)

Example of global filter registration:

```ts
//source file: src/main.ts

async function bootstrap() {
  const app = await NestFactory.create<NestExpressApplication>(AppModule);

  app.use(appsignalExpressMiddleware(appsignal));
  app.useGlobalFilters(new AllExceptionFilter());

  await app.listen(3000);
}
bootstrap();
```

[source code](https://github.com/superfaceai/nestjs-appsignal-example/blob/4432a20e8452f08e5edaecb9e54b4d389f286847/src/main.ts)

## Monitoring `@nestjs/bull` processes

In addition to NestJS we also use [Bull](https://github.com/OptimalBits/bull) for background jobs processing. NestJS provides `@nestjs/bull` package as a wrapper of for Bull.

AppSignal doesn't trace Bull jobs automatically. Fortunately, we can use [Appsignal custom instrumentation](https://docs.appsignal.com/nodejs/instrumentation/) to handle tracing ourselves.

To trace Bull jobs, we have created a Bull process decorator `ProcessMonitor`:

```ts
//source file: src/bull/process_monitor.decorator.ts

export function ProcessMonitor(): MethodDecorator {
  return function (
    target,
    propertyKey: string,
    descriptor: PropertyDescriptor
  ) {
    const method = descriptor.value;

    descriptor.value = async function (...args: any) {
      const tracer = appsignal.tracer();

      const span = tracer.createSpan({
        namespace: 'worker',
      });
      span.setName(`JOB ${this.constructor.name}.${propertyKey}`);
      span.setCategory('job.handler');

      const job = args[0];

      if (job) {
        span.setSampleData('custom_data', { jobId: job.id });
      }

      let result;
      await tracer.withSpan(span, async span => {
        try {
          result = await method.bind(this).apply(target, args);
        } catch (error) {
          span.setError(error);
          throw error;
        } finally {
          span.close();
        }
      });

      return result;
    };
  };
}
```

[source code](https://github.com/superfaceai/nestjs-appsignal-example/blob/7f6aa2fcdfb38a86b7cbd5bada4e39d081e9bbf0/src/bull/process_monitor.decorator.ts)

The `ProcessMonitor` method decorator creates new span in `worker` namespace, collects job ID, sets span with the error in case an exception occurs.

Once you add `ProcessMonitor` decorator into your code base, start using it by decorating your Bull queue processor method:

```ts
export const MAILING_QUEUE = 'mails';
export const SEND_EMAIL = 'send_email';

@Processor(MAILING_QUEUE)
export class MailingProcessor {

  @Process(SEND_EMAIL)
  @ProcessMonitor()
  async sendEmail(job: Job) {
    ...
  }
}
```

## Graceful AppSignal stopping

By default, `@appsignal/nodejs` starts [minutely probes](https://docs.appsignal.com/nodejs/instrumentation/minutely-probes.html), which keep track of Node.js V8 heap statistics. This feature gives you insights about Node.js internals.


![Node.js V8 heap statistics in AppSignal Dashboard](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/w87mg7qgin3owbew9l28.jpg)

Unfortunately, with minutely probes enabled, you have to explicitly stop the probes by calling the `stop` method. Otherwise your application process won't stop gracefully.

Nest comes with the `onApplicationShutdown` [lifecycle event](https://docs.nestjs.com/fundamentals/lifecycle-events#lifecycle-events-1), which is the right place to call AppSignal stop method. See example of `AppsignalShutdownService` implementation below:

```ts
//source file: src/appsignal_shutdown.service.ts

@Injectable()
export class AppsignalShutdownService implements OnApplicationShutdown {
  onApplicationShutdown(_signal: string) {
    appsignal.stop();
  }
}
```

[source code](https://github.com/superfaceai/nestjs-appsignal-example/blob/4432a20e8452f08e5edaecb9e54b4d389f286847/src/appsignal_shutdown.service.ts)

Do not forget to add `AppsignalShutdownService` in your Nest application module.

```ts
//source file: src/app.module.ts

@Module({
  providers: [AppsignalShutdownService],
})
export class AppModule {}
```

[source code](https://github.com/superfaceai/nestjs-appsignal-example/blob/4432a20e8452f08e5edaecb9e54b4d389f286847/src/app.module.ts)