mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
feat: Add central error handler
Added:
- Added Midleware to capture and handle all errors
- will catch error from `throw ...` and `next(...)`
- Added RequestError base error class to build consistent error objects
- Added common errors like `UnauthorizedRequestError`, `BadRequestError`
- Added consistent Logging solution using `winston`
- Supports Loki Transporter using `winston-loki`
- Outputing Legal disclaimer to console when `TELEMETRY_ENABLED=true`
Changed:
- Changed console.log to getLogger() favor of using consistent logging
This commit is contained in:
2
backend/environment.d.ts
vendored
2
backend/environment.d.ts
vendored
@@ -14,6 +14,8 @@ declare global {
|
|||||||
JWT_SIGNUP_SECRET: string;
|
JWT_SIGNUP_SECRET: string;
|
||||||
MONGO_URL: string;
|
MONGO_URL: string;
|
||||||
NODE_ENV: 'development' | 'staging' | 'testing' | 'production';
|
NODE_ENV: 'development' | 'staging' | 'testing' | 'production';
|
||||||
|
VERBOSE_ERROR_OUTPUT: boolean;
|
||||||
|
LOKI_HOST: string;
|
||||||
CLIENT_ID_HEROKU: string;
|
CLIENT_ID_HEROKU: string;
|
||||||
CLIENT_ID_VERCEL: string;
|
CLIENT_ID_VERCEL: string;
|
||||||
CLIENT_ID_NETLIFY: string;
|
CLIENT_ID_NETLIFY: string;
|
||||||
|
|||||||
11475
backend/package-lock.json
generated
11475
backend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -25,7 +25,9 @@
|
|||||||
"stripe": "^10.7.0",
|
"stripe": "^10.7.0",
|
||||||
"tweetnacl": "^1.0.3",
|
"tweetnacl": "^1.0.3",
|
||||||
"tweetnacl-util": "^0.15.1",
|
"tweetnacl-util": "^0.15.1",
|
||||||
"typescript": "^4.9.3"
|
"typescript": "^4.9.3",
|
||||||
|
"winston": "^3.8.2",
|
||||||
|
"winston-loki": "^6.0.6"
|
||||||
},
|
},
|
||||||
"name": "infisical-api",
|
"name": "infisical-api",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
/* eslint-disable no-console */
|
|
||||||
|
|
||||||
import express from 'express';
|
import express, { NextFunction, Request, Response } from 'express';
|
||||||
import helmet from 'helmet';
|
import helmet from 'helmet';
|
||||||
import cors from 'cors';
|
import cors from 'cors';
|
||||||
import cookieParser from 'cookie-parser';
|
import cookieParser from 'cookie-parser';
|
||||||
@@ -29,6 +28,9 @@ import {
|
|||||||
integration as integrationRouter,
|
integration as integrationRouter,
|
||||||
integrationAuth as integrationAuthRouter
|
integrationAuth as integrationAuthRouter
|
||||||
} from './routes';
|
} from './routes';
|
||||||
|
import { getLogger } from './utils/logger';
|
||||||
|
import RequestError from './utils/requestError';
|
||||||
|
import { InternalServerError } from './utils/errors';
|
||||||
|
|
||||||
export const app = express();
|
export const app = express();
|
||||||
|
|
||||||
@@ -50,6 +52,19 @@ if (NODE_ENV === 'production') {
|
|||||||
app.use(helmet());
|
app.use(helmet());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//* Error Handling Middleware
|
||||||
|
app.use((error: RequestError|Error, req: Request, res: Response, next: NextFunction)=>{
|
||||||
|
if(res.headersSent) return next();
|
||||||
|
|
||||||
|
if(!(error instanceof RequestError)){
|
||||||
|
error = InternalServerError({context: {exception: error.message}, stack: error.stack})
|
||||||
|
getLogger('backend-main').log((<RequestError>error).levelName.toLowerCase(), (<RequestError>error).message)
|
||||||
|
}
|
||||||
|
res.status((<RequestError>error).statusCode).json((<RequestError>error).format(req))
|
||||||
|
next()
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
// routers
|
// routers
|
||||||
app.use('/api/v1/signup', signupRouter);
|
app.use('/api/v1/signup', signupRouter);
|
||||||
app.use('/api/v1/auth', authRouter);
|
app.use('/api/v1/auth', authRouter);
|
||||||
@@ -70,5 +85,5 @@ app.use('/api/v1/integration', integrationRouter);
|
|||||||
app.use('/api/v1/integration-auth', integrationAuthRouter);
|
app.use('/api/v1/integration-auth', integrationAuthRouter);
|
||||||
|
|
||||||
export const server = app.listen(PORT, () => {
|
export const server = app.listen(PORT, () => {
|
||||||
console.log(`Listening on PORT ${[PORT]}`);
|
getLogger("backend-main").info(`Server started listening at port ${PORT}`)
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ const JWT_SIGNUP_LIFETIME = process.env.JWT_SIGNUP_LIFETIME! || '15m';
|
|||||||
const JWT_SIGNUP_SECRET = process.env.JWT_SIGNUP_SECRET!;
|
const JWT_SIGNUP_SECRET = process.env.JWT_SIGNUP_SECRET!;
|
||||||
const MONGO_URL = process.env.MONGO_URL!;
|
const MONGO_URL = process.env.MONGO_URL!;
|
||||||
const NODE_ENV = process.env.NODE_ENV! || 'production';
|
const NODE_ENV = process.env.NODE_ENV! || 'production';
|
||||||
|
const VERBOSE_ERROR_OUTPUT = process.env.VERBOSE_ERROR_OUTPUT || false;
|
||||||
|
const LOKI_HOST = process.env.LOKI_HOST || undefined;
|
||||||
const CLIENT_SECRET_HEROKU = process.env.CLIENT_SECRET_HEROKU!;
|
const CLIENT_SECRET_HEROKU = process.env.CLIENT_SECRET_HEROKU!;
|
||||||
const CLIENT_ID_HEROKU = process.env.CLIENT_ID_HEROKU!;
|
const CLIENT_ID_HEROKU = process.env.CLIENT_ID_HEROKU!;
|
||||||
const CLIENT_ID_VERCEL = process.env.CLIENT_ID_VERCEL!;
|
const CLIENT_ID_VERCEL = process.env.CLIENT_ID_VERCEL!;
|
||||||
@@ -51,6 +53,8 @@ export {
|
|||||||
JWT_SIGNUP_SECRET,
|
JWT_SIGNUP_SECRET,
|
||||||
MONGO_URL,
|
MONGO_URL,
|
||||||
NODE_ENV,
|
NODE_ENV,
|
||||||
|
VERBOSE_ERROR_OUTPUT,
|
||||||
|
LOKI_HOST,
|
||||||
CLIENT_ID_HEROKU,
|
CLIENT_ID_HEROKU,
|
||||||
CLIENT_ID_VERCEL,
|
CLIENT_ID_VERCEL,
|
||||||
CLIENT_ID_NETLIFY,
|
CLIENT_ID_NETLIFY,
|
||||||
|
|||||||
@@ -5,8 +5,16 @@ import {
|
|||||||
POSTHOG_PROJECT_API_KEY,
|
POSTHOG_PROJECT_API_KEY,
|
||||||
TELEMETRY_ENABLED
|
TELEMETRY_ENABLED
|
||||||
} from '../config';
|
} from '../config';
|
||||||
|
import { getLogger } from '../utils/logger';
|
||||||
|
|
||||||
console.log('TELEMETRY_ENABLED: ', TELEMETRY_ENABLED);
|
if(TELEMETRY_ENABLED){
|
||||||
|
getLogger("backend-main").info([
|
||||||
|
"",
|
||||||
|
"Infisical collects telemetry data about general usage.",
|
||||||
|
"The data helps us understand how the product is doing and guide our product development to create the best possible platform; it also helps us demonstrate growth for investors as we support Infisical as open-source software.",
|
||||||
|
"To opt out of telemetry, you can set `TELEMETRY_ENABLED=false` within the environment variables",
|
||||||
|
].join('\n'))
|
||||||
|
}
|
||||||
|
|
||||||
let postHogClient: any;
|
let postHogClient: any;
|
||||||
if (NODE_ENV === 'production' && TELEMETRY_ENABLED) {
|
if (NODE_ENV === 'production' && TELEMETRY_ENABLED) {
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
/* eslint-disable no-console */
|
|
||||||
import mongoose from 'mongoose';
|
import mongoose from 'mongoose';
|
||||||
|
import { getLogger } from '../utils/logger';
|
||||||
|
|
||||||
export const initDatabase = (MONGO_URL: string) => {
|
export const initDatabase = (MONGO_URL: string) => {
|
||||||
mongoose
|
mongoose
|
||||||
.connect(MONGO_URL)
|
.connect(MONGO_URL)
|
||||||
.then(() => console.log('Successfully connected to DB'))
|
.then(() => getLogger("database").info("Database connection established"))
|
||||||
.catch((e) => console.log('Failed to connect to DB ', e));
|
.catch((e) => getLogger("database").error(`Unable to establish Database connection due to the error.\n${e}`));
|
||||||
return mongoose.connection;
|
return mongoose.connection;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
/* eslint-disable no-console */
|
|
||||||
import mongoose from 'mongoose';
|
import mongoose from 'mongoose';
|
||||||
import { createTerminus } from '@godaddy/terminus';
|
import { createTerminus } from '@godaddy/terminus';
|
||||||
|
import { getLogger } from '../utils/logger';
|
||||||
|
|
||||||
export const setUpHealthEndpoint = <T>(server: T) => {
|
export const setUpHealthEndpoint = <T>(server: T) => {
|
||||||
const onSignal = () => {
|
const onSignal = () => {
|
||||||
console.log('Server is starting clean-up');
|
getLogger('backend-main').info('Server is starting clean-up');
|
||||||
return Promise.all([
|
return Promise.all([
|
||||||
new Promise((resolve) => {
|
new Promise((resolve) => {
|
||||||
if (mongoose.connection && mongoose.connection.readyState == 1) {
|
if (mongoose.connection && mongoose.connection.readyState == 1) {
|
||||||
|
|||||||
1
backend/src/types/express/index.d.ts
vendored
1
backend/src/types/express/index.d.ts
vendored
@@ -1,5 +1,6 @@
|
|||||||
import * as express from 'express';
|
import * as express from 'express';
|
||||||
|
|
||||||
|
|
||||||
// TODO: fix (any) types
|
// TODO: fix (any) types
|
||||||
declare global {
|
declare global {
|
||||||
namespace Express {
|
namespace Express {
|
||||||
|
|||||||
64
backend/src/utils/errors.ts
Normal file
64
backend/src/utils/errors.ts
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import RequestError, { LogLevel, RequestErrorContext } from "./requestError"
|
||||||
|
|
||||||
|
export const RouteNotFoundError = (error?: Partial<RequestErrorContext>) => new RequestError({
|
||||||
|
logLevel: error?.logLevel ?? LogLevel.INFO,
|
||||||
|
statusCode: error?.statusCode ?? 404,
|
||||||
|
type: error?.type ?? 'route_not_found',
|
||||||
|
message: error?.message ?? 'The requested source was not found',
|
||||||
|
context: error?.context,
|
||||||
|
stack: error?.stack
|
||||||
|
})
|
||||||
|
|
||||||
|
export const MethodNotAllowedError = (error?: Partial<RequestErrorContext>) => new RequestError({
|
||||||
|
logLevel: error?.logLevel ?? LogLevel.INFO,
|
||||||
|
statusCode: error?.statusCode ?? 405,
|
||||||
|
type: error?.type ?? 'method_not_allowed',
|
||||||
|
message: error?.message ?? 'The requested method is not allowed for the resource',
|
||||||
|
context: error?.context,
|
||||||
|
stack: error?.stack
|
||||||
|
})
|
||||||
|
|
||||||
|
export const UnauthorizedRequestError = (error?: Partial<RequestErrorContext>) => new RequestError({
|
||||||
|
logLevel: error?.logLevel ?? LogLevel.INFO,
|
||||||
|
statusCode: error?.statusCode ?? 401,
|
||||||
|
type: error?.type ?? 'unauthorized',
|
||||||
|
message: error?.message ?? 'You are not authorized to access this resource',
|
||||||
|
context: error?.context,
|
||||||
|
stack: error?.stack
|
||||||
|
})
|
||||||
|
|
||||||
|
export const ForbiddenRequestError = (error?: Partial<RequestErrorContext>) => new RequestError({
|
||||||
|
logLevel: error?.logLevel ?? LogLevel.INFO,
|
||||||
|
statusCode: error?.statusCode ?? 403,
|
||||||
|
type: error?.type ?? 'forbidden',
|
||||||
|
message: error?.message ?? 'You are not allowed to access this resource',
|
||||||
|
context: error?.context,
|
||||||
|
stack: error?.stack
|
||||||
|
})
|
||||||
|
|
||||||
|
export const BadRequestError = (error?: Partial<RequestErrorContext>) => new RequestError({
|
||||||
|
logLevel: error?.logLevel ?? LogLevel.INFO,
|
||||||
|
statusCode: error?.statusCode ?? 400,
|
||||||
|
type: error?.type ?? 'bad_request',
|
||||||
|
message: error?.message ?? 'The request is invalid or cannot be served',
|
||||||
|
context: error?.context,
|
||||||
|
stack: error?.stack
|
||||||
|
})
|
||||||
|
|
||||||
|
export const InternalServerError = (error?: Partial<RequestErrorContext>) => new RequestError({
|
||||||
|
logLevel: error?.logLevel ?? LogLevel.ERROR,
|
||||||
|
statusCode: error?.statusCode ?? 500,
|
||||||
|
type: error?.type ?? 'internal_server_error',
|
||||||
|
message: error?.message ?? 'The server encountered an error while processing the request',
|
||||||
|
context: error?.context,
|
||||||
|
stack: error?.stack
|
||||||
|
})
|
||||||
|
|
||||||
|
export const ServiceUnavailableError = (error?: Partial<RequestErrorContext>) => new RequestError({
|
||||||
|
logLevel: error?.logLevel ?? LogLevel.ERROR,
|
||||||
|
statusCode: error?.statusCode ?? 503,
|
||||||
|
type: error?.type ?? 'service_unavailable',
|
||||||
|
message: error?.message ?? 'The service is currently unavailable. Please try again later.',
|
||||||
|
context: error?.context,
|
||||||
|
stack: error?.stack
|
||||||
|
})
|
||||||
65
backend/src/utils/logger.ts
Normal file
65
backend/src/utils/logger.ts
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
/* eslint-disable no-console */
|
||||||
|
import { createLogger, format, transports } from 'winston';
|
||||||
|
import LokiTransport from 'winston-loki';
|
||||||
|
import { LOKI_HOST, NODE_ENV } from '../config';
|
||||||
|
|
||||||
|
const { combine, colorize, label, printf, splat, timestamp } = format;
|
||||||
|
|
||||||
|
const logFormat = (prefix: string) => combine(
|
||||||
|
timestamp(),
|
||||||
|
splat(),
|
||||||
|
label({ label: prefix }),
|
||||||
|
printf((info) => `${info.timestamp} ${info.label} ${info.level}: ${info.message}`)
|
||||||
|
);
|
||||||
|
|
||||||
|
const createLoggerWithLabel = (level: string, label: string) => {
|
||||||
|
const _level = level.toLowerCase() || 'info'
|
||||||
|
//* Always add Console output to transports
|
||||||
|
const _transports: any[] = [
|
||||||
|
new transports.Console({
|
||||||
|
format: combine(
|
||||||
|
colorize(),
|
||||||
|
logFormat(label),
|
||||||
|
// format.json()
|
||||||
|
)
|
||||||
|
})
|
||||||
|
]
|
||||||
|
//* Add LokiTransport if it's enabled
|
||||||
|
if(LOKI_HOST !== undefined){
|
||||||
|
_transports.push(
|
||||||
|
new LokiTransport({
|
||||||
|
host: LOKI_HOST,
|
||||||
|
handleExceptions: true,
|
||||||
|
handleRejections: true,
|
||||||
|
batching: true,
|
||||||
|
level: _level,
|
||||||
|
timeout: 30000,
|
||||||
|
format: format.combine(
|
||||||
|
format.json()
|
||||||
|
),
|
||||||
|
labels: {app: process.env.npm_package_name, version: process.env.npm_package_version, environment: NODE_ENV},
|
||||||
|
onConnectionError: (err: Error)=> console.error('Connection error while connecting to Loki Server.\n', err)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return createLogger({
|
||||||
|
level: _level,
|
||||||
|
transports: _transports,
|
||||||
|
format: format.combine(
|
||||||
|
logFormat(label),
|
||||||
|
format.metadata({ fillExcept: ['message', 'level', 'timestamp', 'label'] })
|
||||||
|
)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const DEFAULT_LOGGERS = {
|
||||||
|
"backend-main": createLoggerWithLabel('info', '[IFSC:backend-main]'),
|
||||||
|
"database": createLoggerWithLabel('info', '[IFSC:database]'),
|
||||||
|
}
|
||||||
|
type LoggerNames = keyof typeof DEFAULT_LOGGERS
|
||||||
|
|
||||||
|
export const getLogger = (loggerName: LoggerNames) => {
|
||||||
|
return DEFAULT_LOGGERS[loggerName]
|
||||||
|
}
|
||||||
113
backend/src/utils/requestError.ts
Normal file
113
backend/src/utils/requestError.ts
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
import { Request } from 'express'
|
||||||
|
import { VERBOSE_ERROR_OUTPUT } from '../config'
|
||||||
|
|
||||||
|
export enum LogLevel {
|
||||||
|
DEBUG = 100,
|
||||||
|
INFO = 200,
|
||||||
|
NOTICE = 250,
|
||||||
|
WARNING = 300,
|
||||||
|
ERROR = 400,
|
||||||
|
CRITICAL = 500,
|
||||||
|
ALERT = 550,
|
||||||
|
EMERGENCY = 600,
|
||||||
|
}
|
||||||
|
|
||||||
|
export type RequestErrorContext = {
|
||||||
|
logLevel?: LogLevel,
|
||||||
|
statusCode: number,
|
||||||
|
type: string,
|
||||||
|
message: string,
|
||||||
|
context?: Record<string, unknown>,
|
||||||
|
stack?: string|undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class RequestError extends Error{
|
||||||
|
|
||||||
|
private _logLevel: LogLevel
|
||||||
|
private _logName: string
|
||||||
|
statusCode: number
|
||||||
|
type: string
|
||||||
|
context: Record<string, unknown>
|
||||||
|
extra: Record<string, string|number|symbol>[]
|
||||||
|
private stacktrace: string|undefined|string[]
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
{logLevel, statusCode, type, message, context, stack} : RequestErrorContext
|
||||||
|
){
|
||||||
|
super(message)
|
||||||
|
this._logLevel = logLevel || LogLevel.INFO
|
||||||
|
this._logName = LogLevel[this._logLevel]
|
||||||
|
this.statusCode = statusCode
|
||||||
|
this.type = type
|
||||||
|
this.context = context || {}
|
||||||
|
this.extra = []
|
||||||
|
|
||||||
|
if(stack) this.stack = stack
|
||||||
|
else Error.captureStackTrace(this, this.constructor)
|
||||||
|
this.stacktrace = this.stack?.split('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
static convertFrom(error: Error) {
|
||||||
|
//This error was not handled by error handler. Please report this incident to the staff.
|
||||||
|
return new RequestError({
|
||||||
|
logLevel: LogLevel.ERROR,
|
||||||
|
statusCode: 500,
|
||||||
|
type: 'internal_server_error',
|
||||||
|
message: 'This error was not handled by error handler. Please report this incident to the staff',
|
||||||
|
context: {
|
||||||
|
message: error.message,
|
||||||
|
name: error.name
|
||||||
|
},
|
||||||
|
stack: error.stack
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
get level(){ return this._logLevel }
|
||||||
|
get levelName(){ return this._logName }
|
||||||
|
|
||||||
|
withTags(...tags: string[]|number[]){
|
||||||
|
this.context['tags'] = Object.assign(tags, this.context['tags'])
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
withExtras(...extras: Record<string, string|boolean|number>[]){
|
||||||
|
this.extra = Object.assign(extras, this.extra)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
private _omit(obj: any, keys: string[]): typeof obj{
|
||||||
|
const exclude = new Set(keys)
|
||||||
|
obj = Object.fromEntries(Object.entries(obj).filter(e => !exclude.has(e[0])))
|
||||||
|
return obj
|
||||||
|
}
|
||||||
|
|
||||||
|
public format(req: Request){
|
||||||
|
let _context = Object.assign({
|
||||||
|
stacktrace: this.stacktrace
|
||||||
|
}, this.context)
|
||||||
|
|
||||||
|
//* Omit sensitive information from context that can leak internal workings of this program if user is not developer
|
||||||
|
if(!VERBOSE_ERROR_OUTPUT){
|
||||||
|
_context = this._omit(_context, [
|
||||||
|
'stacktrace',
|
||||||
|
'exception',
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatObject = {
|
||||||
|
type: this.type,
|
||||||
|
message: this.message,
|
||||||
|
context: _context,
|
||||||
|
level: this.level,
|
||||||
|
level_name: this.levelName,
|
||||||
|
status_code: this.statusCode,
|
||||||
|
datetime_iso: new Date().toISOString(),
|
||||||
|
application: process.env.npm_package_name || 'unknown',
|
||||||
|
request_id: req.headers["Request-Id"],
|
||||||
|
extra: this.extra
|
||||||
|
}
|
||||||
|
|
||||||
|
return formatObject
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user