I am using NestJS to build the rest APIs and trying to implement logging using Sentry but did not get the right way of formatting logs before sending them to sentry.
I already have a filter that catches every kind of log (HTTP as well as application errors). And I'm trying to implement sentry globally without having it to inject into each controller and service.
error-filter.ts
import {
HttpExceptionResponse,
CustomeHttpExceptionResponse,
} from './modals/http-exception-response.interface';
import {
ExceptionFilter,
Catch,
ArgumentsHost,
HttpException,
HttpStatus,
} from '#nestjs/common';
import * as fs from 'fs';
import { Request } from 'express';
import { captureException } from '#sentry/node';
#Catch()
export class HttpErrorFilter implements ExceptionFilter {
catch(exception: unknown, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse();
const request = ctx.getRequest();
let status: HttpStatus;
let errorMessage: string;
if (exception instanceof HttpException) {
status = exception.getStatus();
const errorResponse = exception.getResponse();
errorMessage =
(errorResponse as HttpExceptionResponse).error ||
exception.message ||
"Sorry, can't process this request at the moment!";
} else {
status = HttpStatus.INTERNAL_SERVER_ERROR;
errorMessage = 'Internal server error!';
}
const errorResponse = this.getErrorResponse(status, errorMessage, request);
const errorLog = this.getErrorLog(errorResponse, request, exception);
// this.writeErrorLogToFile(errorLog);
this.sendLogsToSentry(errorLog);
response.status(status).json(errorResponse);
}
private getErrorResponse = (
status: HttpStatus,
errorMessage: string,
request: Request,
): CustomeHttpExceptionResponse => ({
statusCode: status,
error: errorMessage,
path: request.url,
method: request.method,
timeStamp: new Date(),
message: errorMessage,
});
private getErrorLog = (
errorResponse: CustomeHttpExceptionResponse,
request: Request,
exception: unknown,
): string => {
const { statusCode, error } = errorResponse;
const { method, url } = request;
const errorLog = ` Response code: ${statusCode} - Method: ${method} - URL: ${url}\n
${JSON.stringify(errorResponse)}\n
User: ${JSON.stringify(request.user ?? 'User not signed in')}\n
Details: ${
exception instanceof HttpException ? exception.stack : error
}\n\n`;
return errorLog;
};
private writeErrorLogToFile = (errorLog: string): void => {
fs.appendFile('error.log', errorLog, 'utf8', (err) => {
if (err) throw err;
});
};
private sendLogsToSentry = async (errorLog: any) => {
captureException(errorLog);
};
}
main.ts
....
// Sentry setup for logging
const configService = app.get(ConfigService);
const sentryClientKey = configService.get('SENTRY_CLIENT_KEY');
Sentry.init({
dsn: sentryClientKey,
tracesSampleRate: 1.0,
});
These are the only two files I tried for setting up the sentry.
.....
With this, the errors are being successfully captured but not in the way they have to.
I can't get the full information like Stacktrace for non-HTTP errors, no user info, and many other missing pieces. I know that I've constructed the log as a string and sent it to Sentry but I tried sending the whole exception object too, and that event didn't work.
I tried looking into the documentation but couldn't figure it out. So how the logs should be formatted to get the most out of it in sentry? I am searching for a solution with less code change. But if it isn't possible without injecting Sentry into the service and controller, will do that. Please guide me on how.
Related
In one of my nestjs projects I have created the prisma custom exception filters.
This code works properly for http request. But while I use cron then this does not work for me. In cron if any prisma error occurs then it stops execution and throw PrismaClientKnownRequestError error in the console directly.
Did I do any mistake? Or exception filter does not work in cron? Please help me! Any help will be appreciated. Thanks.
Prisma exception filter code:
#Catch(Prisma.PrismaClientKnownRequestError)
export class PrismaClientKnownRequestErrorExceptionFilter extends BaseExceptionFilter {
logger = new Logger('PrismaClient');
catch(
exception: Prisma.PrismaClientKnownRequestError,
host: ArgumentsHost,
): void {
const ctx: HttpArgumentsHost = host.switchToHttp();
const request: Request = ctx.getRequest<Request>();
const response: Response = ctx.getResponse<Response>();
let statusCode: HttpStatus;
let message: string;
switch (exception.code) {
case 'P2002': // unique constraint or duplication
statusCode = HttpStatus.CONFLICT;
message = `Unique constraint failed on the constraint: ${exception?.meta?.['target']}`;
break;
....
default:
// super.catch(exception, host);
statusCode = HttpStatus.INTERNAL_SERVER_ERROR;
message = exception.message.replace(/\r/g, '').replace(/\n/g, '');
break;
}
this.logger.error(message);
this.response(statusCode, message, request, response);
}
response(
statusCode: HttpStatus,
message: string,
request: Request,
response: Response,
) {
response.status(statusCode).json({
statusCode,
message,
timestamp: new Date().toISOString(),
path: request.url,
});
}
}
I added the filter in app module as provider like -
{
provide: APP_FILTER,
useClass: PrismaClientKnownRequestErrorExceptionFilter,
}
I have a Pub/Sub triggered cloud function that calls an API end-point and logs the message. But I am not seeing all log messages being logged in console except everything right before calling API.
Once the API is called I am logging the response, and exception messages in case of any error.
It is logging: Function execution took 120015 ms. Finished with status: timeout Earlier the default timeout was set to 60 sec, later I increased it to 120 sec. Still the problem persist.
I am not understanding the issue here since it is working locally without any issues.
Here I have custom module to log messages to Winston and GCP console (it doesn't have any issue and working fine).
Code calling the API module:
const console = require('./logging-utils');
const portal_api = require('./api-utils');
exports.triggerPortalNotifier = async (event, context) => {
try {
/*
.....
*/
console.metadata.cloudFunction = cf_name;
console.metadata.requestId = requestId;
console.metadata.organizationId = organizationId;
console.metadata.instanceId = instanceId;
console.logMessage(`Event received with payload: some message`);
var payload = {
//payload to API
}
var response = await portal_api.notifyPortal(payload);
console.logMessage(`Response received from portal API is: ${JSON.stringify(response.data)}`);
}
else {
throw new Error(`Invalid message received: ${_message}`);
}
}
catch (error) {
console.logMessage(`Portal API failed with exception: ${error}`);
throw new Error(`${error.message}`);
}
}
Code that make API request (using axios module)
require('dotenv').config();
const axios = require('./axios-instance');
const console = require('./logging-utils');
const nextgen_api = {
notifyPortal: async (payload) => {
try {
const config = {
headers: {
'Authorization': process.env.PORTAL_AUTHORIZATION_TOKEN,
'Content-Type': "application/json",
'Accept': "application/plain"
}
}
console.logMessage(`Input paylod for API end-point: ${process.env.PORTAL_API} => ${JSON.stringify(payload)}`)
const response = await axios.post(process.env.PORTAL_API, JSON.parse(JSON.stringify(payload)), config);
console.logMessage(`Response from API: ${JSON.stringify(response.data)}`);
return response;
}
catch (err) {
if (err.response && err.response.status !== 200) {
console.logMessage(`API call failed with status code: ${err.response.status} `);
throw new Error(`API call failed with status code: ${err.response.status} `);
}
else {
console.logMessage(`API call failed with ${err.stack}`);
throw new Error(`API call failed with status code: ${err.stack} `);
}
}
}
}
module.exports = my_api;
Message Response from API: ${JSON.stringify(response.data)} is not being logged.
Any help here is appreciated.
When an unhandled error occurs, the front side gets a 500 error without any information about the error. It just receives this:
{
"statusCode": 500,
"message": "Internal server error"
}
But when I check the console, I see what happened and what the error message is. It's ok in the development environment but it's hard to figure out in production. How can I return a complete error message in production instead of just a simple message like "Internal server error"?
Generally you should be doing error handling around things that have a chance to go wrong (database operations, sending emails, validations), but if you're looking for a general error handling mechanism, you can use the Exception Filters construct that Nest provides to catch errors and manage them as necessary.
NestJS has the NotFoundException you can use.
You can implement your own custom Exception filters and add it to your controllers to capture a NotFoundException or whatever communication protocol, but this is assuming that you have an HTTP communication protocol.
So if http is the only communication protocol you are dealing with then you can just write this into one of your methods inside your service:
if (!user) {
throw new NotFoundException('user not found');
}
And you would import that from:
import { Injectable, NotFoundException } from '#nestjs/common';
Finally found the answer to this question - you don't need any complicated exception filters, or interceptors, etc. You just need to wrap the code which is causing the 500 in an if statement, and throw a HttpException - note, throwing an Error will not work.
In other words, you need to resolve this error in your service class, not in your controller.
For example, in your service class:
async getUsers(){
let response = doSomething();
if (response.status !== 200) {
throw new HttpException(
`Error getting users: ${response.status} ${response.statusText}`,
response.status,
)
} else {
return response.data
}
}
//InternalServerError.filter.ts
import {
InternalServerErrorException,
Catch,
ArgumentsHost,
ExceptionFilter,
HttpStatus,
HttpException
} from "#nestjs/common";
// catch all error
#Catch()
export default class InternalServerErrorExceptionFilter implements ExceptionFilter {
catch(exception: Error, host: ArgumentsHost) {
let { message: errMsg, stack: errStack, name: errName } = exception;
// let errRes = exception.getResponse();
// let errCode = exception.getStatus();
let ctx = host.switchToHttp();
let req = ctx.getRequest();
let res = ctx.getResponse();
const statusName = "系统内部错误";
res.statusCode = HttpStatus.INTERNAL_SERVER_ERROR;
// HttpException Error
if (exception instanceof HttpException) {
// set httpException res to res
res.status(exception.getStatus()).json(exception.getResponse());
return;
}
// other error to rewirte InternalServerErrorException response
res.render("Error.ejs", {
exception,
errMsg,
errStack,
errName,
statusCode: res.statusCode,
statusName,
req
});
}
}
then use global filter provider for InternalServerError.filter.ts
// app.module.ts
#Module({
providers:[{
provide: APP_FILTER,
useClass: InternalServerErrorExceptionFilter
}])
The why
We're using the axios-retry library, which uses this code internally:
axios.interceptors.response.use(null, error => {
Since it only specifies the error callback, the Axios documentation says:
Any status codes that falls outside the range of 2xx cause this function to trigger
Unfortunately we're calling a non-RESTful API that can return 200 with an error code in the body, and we need to retry that.
We've tried adding an Axios interceptor before axios-retry does and changing the result status in this case; that did not trigger the subsequent interceptor error callback though.
What did work was specifying a custom adapter. However this is not well-documented and our code does not handle every case.
The code
const axios = require('axios');
const httpAdapter = require('axios/lib/adapters/http');
const settle = require('axios/lib/core/settle');
const axiosRetry = require('axios-retry');
const myAdapter = async function(config) {
return new Promise((resolve, reject) => {
// Delegate to default http adapter
return httpAdapter(config).then(result => {
// We would have more logic here in the production code
if (result.status === 200) result.status = 500;
settle(resolve, reject, result);
return result;
});
});
}
const axios2 = axios.create({
adapter: myAdapter
});
function isErr(error) {
console.log('retry checking response', error.response.status);
return !error.response || (error.response.status === 500);
}
axiosRetry(axios2, {
retries: 3,
retryCondition: isErr
});
// httpstat.us can return various status codes for testing
axios2.get('http://httpstat.us/200')
.then(result => {
console.log('Result:', result.data);
})
.catch(e => console.error('Service returned', e.message));
This works in the error case, printing:
retry checking response 500
retry checking response 500
retry checking response 500
retry checking response 500
Service returned Request failed with status code 500
It works in the success case too (change the URL to http://httpstat.us/201):
Result: { code: 201, description: 'Created' }
The issue
Changing the URL to http://httpstat.us/404, though, results in:
(node:19759) UnhandledPromiseRejectionWarning: Error: Request failed with status code 404
at createError (.../node_modules/axios/lib/core/createError.js:16:15)
at settle (.../node_modules/axios/lib/core/settle.js:18:12)
A catch on the httpAdapter call will catch that error, but how do we pass that down the chain?
What is the correct way to implement an Axios adapter?
If there is a better way to handle this (short of forking the axios-retry library), that would be an acceptable answer.
Update
A coworker figured out that doing .catch(e => reject(e)) (or just .catch(reject)) on the httpAdapter call appears to handle the issue. However we'd still like to have a canonical example of implementing an Axios adapter that wraps the default http adapter.
Here's what worked (in node):
const httpAdapter = require('axios/lib/adapters/http');
const settle = require('axios/lib/core/settle');
const customAdapter = config =>
new Promise((resolve, reject) => {
httpAdapter(config).then(response => {
if (response.status === 200)
// && response.data contains particular error
{
// log if desired
response.status = 503;
}
settle(resolve, reject, response);
}).catch(reject);
});
// Then do axios.create() and pass { adapter: customAdapter }
// Now set up axios-retry and its retryCondition will be checked
Workaround with interceptor and custom error
const axios = require("axios").default;
const axiosRetry = require("axios-retry").default;
axios.interceptors.response.use(async (response) => {
if (response.status == 200) {
const err = new Error("I want to retry");
err.config = response.config; // axios-retry using this
throw err;
}
return response;
});
axiosRetry(axios, {
retries: 1,
retryCondition: (error) => {
console.log("retryCondition");
return false;
},
});
axios
.get("https://example.com/")
.catch((err) => console.log(err.message)); // gonna be here anyway as we'll fail due to interceptor logic
I created a interceptor for my angular project to intercept all the requests and responses, but the function that validates errors in the responses is executed 7 times.
I realized that when I use the throwError of rjxs it performs the function many times, if I use the of rxjs it executes only one, but fails to execute functions that validate errors in subscribes.
constructor(private injector: Injector, public errorHandler: ApplicationErrorHandler) { }
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
const sessaoService = this.injector.get(SessaoService);
if (sessaoService.isLoogedIn()) {
const token = localStorage.getItem('token');
const tokenSplit = token.split(' ');
request = request.clone(
{ setHeaders: { 'Authorization': `${tokenSplit[1]}` } }
);
}
return next.handle(request).pipe(
catchError((err: HttpErrorResponse) => {
console.log('Execute function');
let data = {};
data = {
error: err,
status: err.status,
method: request.method
};
this.errorHandler.handleError(data);
return throwError(err);
})
);
}
I expect that catchError function execute only one time, but it is running 7 times for each request.
My Angular version is: 6.1.3;
My Rxjs version is: 6.4.0;
Sorry for my bad english...
I found the problem, I have a shared module in my application and inside it I had added HTTP_INTERCEPTORS as provider using my class with theintercept function.