It bothered me a bit that, if one of my routes threw an exception, the output could only be logged to stderr. So I did the following to fix this and use Winston for logging instead, while still being fully agnostic to the underlying logging system that is actually used.
Assume that in one of my controllers I have the following REST endpoint:
@post('/myendpoint')
async handleEndpoint(): Promise<void> {
throw new Error('I am an error!');
}
To now add the custom logger, I created a new service for it and bound the Winston variant of it to my application.
src/services/logger.service.ts (the abstract logger service and a concrete implementation of it that uses Winston)
import winston from 'winston';
export interface LoggerService {
logger: object;
}
export class WinstonLoggerService implements LoggerService {
logger: winston.Logger = winston.createLogger({
level: 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json(),
),
transports: [
new winston.transports.Console({
format: winston.format.combine(
winston.format.colorize(),
winston.format.printf(info => {
return `[${info.timestamp}] ${info.level}: ${info.message}`;
}),
),
}),
],
});
}
src/keys.ts
export namespace LoggerBindings {
export const LOGGER = BindingKey.create<LoggerService>('services.logger');
}
src/providers/log-error.provider.ts (a Loopback 4 provider class where the logger class bound by the application is injected into and which can then use it)
import {Provider} from '@loopback/context';
import {LogError, Request} from '@loopback/rest';
import {inject} from '@loopback/core';
import {LoggerBindings} from '../keys';
import {LoggerService} from '../services/logger.service';
export class LogErrorProvider implements Provider<LogError> {
constructor(@inject(LoggerBindings.LOGGER) protected logger: LoggerService) {}
value(): LogError {
return (err, statusCode, req) => this.action(err, statusCode, req);
}
action(err: Error, statusCode: number, req: Request) {
if (statusCode < 500) {
return;
}
this.logger.logger.error(
`HTTP ${statusCode} on ${req.method} ${req.url}: ${err.stack ?? err}`,
);
}
}
src/application.ts (bind statements go into the constructor)
import {WinstonLoggerService} from './services/logger.service';
import {LogErrorProvider} from './providers/log-error.provider';
this.bind(LoggerBindings.LOGGER).toClass(WinstonLoggerService);
this.bind(RestBindings.SequenceActions.LOG_ERROR).toProvider(LogErrorProvider);
The last line in the previous code block is the key here, because it makes sure to bind our custom provider for LOG_ERROR
. Internally, Loopback 4 uses the RejectProvider
defined in @loopback/rest/src/providers/reject.provider.ts
to handle errors thrown in REST endpoints. Into this provider, RestBindings.SequenceActions.LOG_ERROR
is injected, which is taken from @loopback/rest/src/providers/log-error.provider.ts
by default and which we redefine here. This way, we don't need to rewrite the entire reject provider but just the tiny part of it that handles logging REST errors.
When the example route is now invoked, the following is shown on the console:
[2020-01-05T23:41:28.604Z] error: HTTP 500 on POST /myendpoint: Error: I am an error!
at [...long stacktrace...]