diff --git a/README.md b/README.md index 3a58148..c559f62 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ Reusable plugins for Fastify. * [BugSnag Plugin](#bugsnag-plugin) * [Metrics Plugin](#metrics-plugin) * [NewRelic Transaction Manager Plugin](#newrelic-transaction-manager-plugin) + * [UnhandledException Plugin](#unhandledexception-plugin) ## Dependency Management @@ -161,3 +162,78 @@ The plugin decorates your Fastify instance with a `Amplitude`, which you can inj > ``` > "@amplitude/analytics-types": "*" > ``` + +### UnhandledException Plugin + +This plugin provides a mechanism for handling uncaught exceptions within your Fastify application, ensuring that such exceptions are logged and reported. It's especially useful for capturing unforeseen exceptions and provides a controlled shutdown of the Fastify server, thereby ensuring no potential data corruption. + +#### Setup & Configuration + +To integrate this plugin into your Fastify instance, follow these steps: + +1. First, import the necessary types and the plugin: + +```typescript +import { FastifyInstance } from 'fastify'; +import { unhandledExceptionPlugin, ErrorObjectResolver } from '@lokalise/fastify-extras'; +``` + +2. Configure the plugin: + +Define your own `ErrorObjectResolver` to dictate how the uncaught exceptions will be structured for logging. Here's an example: + +```typescript +const myErrorResolver: ErrorObjectResolver = (err, correlationID) => { + return { + error: err, + id: correlationID + }; +}; +``` + +You'll also need to provide an `ErrorReporter` instance. This instance should have a `report` method to handle the error reporting logic. For example: + +```typescript +import { ErrorReporter } from "@lokalise/node-core"; + +const myErrorReporter = new ErrorReporter(/* initialization params */); +``` + +3. Register the plugin with your Fastify instance: + +```typescript +const fastify = Fastify(); + +fastify.register(unhandledExceptionPlugin, { + errorObjectResolver: myErrorResolver, + errorReporter: myErrorReporter +}); +``` + +#### Options + +The plugin accepts the following options: + +- `errorObjectResolver` (required): This function determines the structure of the error object which will be logged in case of an uncaught exception. + +- `errorReporter` (required): An instance of the ErrorReporter which will handle reporting of the uncaught exceptions. + +#### Working Principle + +When an uncaught exception occurs, the plugin: + +- Logs the exception using the provided `errorObjectResolver`. + +- Reports the exception using the `ErrorReporter`. + +- Shuts down the Fastify server gracefully. + +- Exits the process with exit code `1`. + +#### Dependencies + +- `@lokalise/node-core`: For error reporting. + +- `fastify`: The framework this plugin is designed for. + +> 🚨 It's critical to note that this plugin listens to the process's 'uncaughtException' event. Multiple listeners on this event can introduce unpredictable behavior in your application. Ensure that this is the sole listener for this event or handle interactions between multiple listeners carefully. diff --git a/lib/index.ts b/lib/index.ts index ce83358..e84345c 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -53,3 +53,6 @@ export { } from './plugins/amplitudePlugin' export type { FastifyReplyWithPayload } from './types' + +export { unhandledExceptionPlugin } from './plugins/unhandledExceptionPlugin' +export type { UnhandledExceptionPluginOptions } from './plugins/unhandledExceptionPlugin' diff --git a/lib/plugins/unhandledExceptionPlugin.ts b/lib/plugins/unhandledExceptionPlugin.ts new file mode 100644 index 0000000..6fc02c9 --- /dev/null +++ b/lib/plugins/unhandledExceptionPlugin.ts @@ -0,0 +1,29 @@ +import type { ErrorReporter } from '@lokalise/node-core' +import type { FastifyInstance } from 'fastify' +import fp from 'fastify-plugin' + +export type ErrorObjectResolver = (err: unknown, correlationID?: string) => unknown + +export interface UnhandledExceptionPluginOptions { + errorObjectResolver: ErrorObjectResolver + errorReporter: ErrorReporter +} + +function plugin(app: FastifyInstance, opts: UnhandledExceptionPluginOptions) { + // Handle unhandled exceptions + process.on('uncaughtException', (err) => { + const logObject = opts.errorObjectResolver(err) + app.log.fatal(logObject, 'uncaught exception detected') + opts.errorReporter.report({ error: err }) + + // shutdown the server gracefully + app.close(() => { + process.exit(1) // then exit + }) + }) +} + +export const unhandledExceptionPlugin = fp(plugin, { + fastify: '4.x', + name: 'unhandled-exception-plugin', +}) diff --git a/package.json b/package.json index 85787e3..cc2c1c1 100644 --- a/package.json +++ b/package.json @@ -53,8 +53,8 @@ "@opentelemetry/semantic-conventions": "1.17.0", "@prisma/instrumentation": "^5.4.1", "@splitsoftware/splitio": "^10.23.1", - "@amplitude/analytics-node": "^1.3.3", - "fastify-metrics": "^10.3.2", + "@amplitude/analytics-node": "^1.3.4", + "fastify-metrics": "^10.3.3", "fastify-plugin": "^4.5.1", "tslib": "^2.6.2" }, @@ -86,7 +86,8 @@ "shx": "^0.3.4", "ts-node": "^10.9.1", "typescript": "^5.2.2", - "vitest": "^0.34.6" + "vitest": "^0.34.6", + "vite": "4.5.0" }, "engines": { "node": ">=18"