NestJS Observability Utilities
A collection of utilities for logging, error handling, and traces, which fills in some gaps missing from the base NestJS project.
Basic Setup
...
import { HttpAdapterHost } from '@nestjs/core';
import {
OneLineLogger,
ExceptionFilter,
} from '@5stones/nestjs-observability-utilities/logging';
async function bootstrap() {
const app = await NestFactory.create(AppModule, {
logger: new OneLineLogger(),
});
const { httpAdapter } = app.get(HttpAdapterHost);
app.useGlobalFilters(new ExceptionFilter(httpAdapter));
...
}
Logging
For log collection tools like EFK/ELK, Loki, and Cloudwatch Logs, log entries and stack traces should be kept to oneline.
log Tag Function
A string template function which uses JSON.stringify on each parameter,
and has special handling for errors, which includes stack traces as an array.
This can be used with the NestJS logger, but is also useful for CLI output or Error messages.
import { log } from '@5stones/nestjs-observability-utilities/logging';
...
this.logger.warn(log`Something failed ${{ ip }}: ${err}`);
throw new Error(log`Something failed for user: ${user}`);
OneLineLogger Service
Keeps all logs to one line, while still using NestJS's log format.
The stack traces and extra parameters are space separated and stay in one line, similar to the log
tag function.
By default, the LOG_LEVEL
environment variable will be used to determine log levels.
There is also an otelEvents
option to send logs as OpenTelemetry events along with the console logs.
import { OneLineLogger } from '@5stones/nestjs-observability-utilities/logging';
async function bootstrap() {
const app = await NestFactory.create(AppModule, {
logger: new OneLineLogger(undefined, {
}),
});
envLogLevels Function
If you're not using the OneLineLogger, you can still set log levels with an environment variable.
The default is equivalent to setting LOG_LEVEL=log
.
import { envLogLevels } from '@5stones/nestjs-observability-utilities/logging';
async function bootstrap() {
const app = await NestFactory.create(AppModule, {
logger: envLogLevels(),
});
ExceptionFilter
Replaces the default ExceptionHandler to log warnings and include context.
All Errors that bubble up through the controller will be logged.
NestJS HttpExceptions will be logged at the warning level.
Other unhandled errors will be at the error level.
import { HttpAdapterHost } from '@nestjs/core';
import { ExceptionFilter } from '@5stones/nestjs-observability-utilities/logging';
async function bootstrap() {
...
const { httpAdapter } = app.get(HttpAdapterHost);
app.useGlobalFilters(new ExceptionFilter(httpAdapter));
OpenTelemetry
The official library for creating spans does not include a try/catch/finally for error handling and ending spans,
so this fills in those gaps.
withSpan Function
import { trace } from '@opentelemetry/api';
import { withSpan } from '@5stones/nestjs-observability-utilities/otel';
const tracer = trace.getTracer(`your-project.module.YourService`);
...
async someMethod() {
return await withSpan(tracer, 'span name', async (span) => {
...
span.addEvent('some event');
...
return ...
});
@otelSpan Decorator
import { trace } from '@opentelemetry/api';
import { otelSpan } from '@5stones/nestjs-observability-utilities/otel';
const tracer = trace.getTracer(`your-project.module.YourService`);
...
@otelSpan(tracer, 'span name')
async someMethod() {
const span = trace.getActiveSpan();
...
span?.addEvent('some event');
...
return ...
Error Handling in NestJS Microservices
With the built-in error handling, errors in message handlers are not logged,
and then are sent back to the client as an "Internal server error",
giving no indication of what went wrong on either side.
RpcExceptionsFilter
Logs errors with stack traces, and passes back HttpException info.
import { RpcExceptionsFilter } from '@5stones/nestjs-observability-utilities/rpc';
@Controller(...)
@UseFilters(RpcExceptionsFilter)
export class SomeController {
transformError Function
The other side of the RpcExceptionsFilter to get HttpException instances back on the client-side.
const typedError = transformError(genericError);
import { transformError } from '@5stones/nestjs-observability-utilities/rpc';
...
async send<
P extends keyof RpcMessages,
TResult extends RpcMessages[P]['result'],
TInput extends RpcMessages[P]['input'],
>(pattern: P, data: TInput, timeoutMs = 30000): Promise<TResult> {
return await withSpan(tracer, 'rpc ${pattern}', async (span) => {
try {
const result = await firstValueFrom(
this.client.send(pattern, data).pipe(timeout(timeoutMs)),
{ defaultValue: undefined as TResult },
);
span.addEvent('response', {
data: JSON.stringify(data),
result: JSON.stringify(result),
});
return result;
} catch (err) {
throw transformError(err, { logger: this.logger });
}
});
}
Other Useful Utilities
import { ... } from '@5stones/nestjs-observability-utilities';
const path = uri`/something/${email}`;
const value = notNull(valueOrNull);
const value = await notNullFrom(observable);
const email = required('email', data.email);
const myService = requiredService('MyService', this.myService);
const resultArray = await inParallel(items, 10, async (item) => {...});
const obj = await promiseAllEntries({ v1: promise1, v2: promise2 });
const valueOrUndefined = await timeLimitedPromise(somePromise, 500, undefined);
const valueOrNull = await somePromise.catch(handleNotFound);
const newObj = filterEntries(obj, ([key, value], index) => ...);
const newObj = filterKeys(obj, ['key1', 'key2']);
@UseInterceptors(new TimedInterceptor(2000, 60000))
@UseInterceptors(new TimeoutInterceptor(10000, 'Your error message'))
Dev environment
$ npm ci
run the project
// with docker
docker-compose up
// without docker
$ npm run start:dev
Release
The standard release command for this project is:
npm version [<newversion> | major | minor | patch | premajor | preminor | prepatch | prerelease | from-git]
This command will:
- Generate/update the Changelog
- Bump the package version
- Tag & pushing the commit
e.g.
npm version 1.2.17
npm version patch // 1.2.17 -> 1.2.18
License
MIT licensed.