From 27ab1c710fa975b01566b81e24bbebf69c91e381 Mon Sep 17 00:00:00 2001 From: David Teller Date: Mon, 31 Oct 2022 13:09:10 +0100 Subject: [PATCH] Very basic support for Sentry. The Sentry package is very useful for monitoring runtime errors. With this PR, we simply add the necessary mechanism to: - log to sentry any uncaught error that reaches the toplevel, including startup errors; - log to sentry any error that we already log internally as `ERROR`. --- config/default.yaml | 14 +++++++ package.json | 2 + src/ManagementRoomOutput.ts | 13 +++++- src/Mjolnir.ts | 4 +- src/config.ts | 10 +++++ src/index.ts | 11 ++++- src/utils.ts | 28 +++++++++++++ test/integration/mjolnirSetupUtils.ts | 4 +- yarn.lock | 59 ++++++++++++++++++++++++++- 9 files changed, 136 insertions(+), 9 deletions(-) diff --git a/config/default.yaml b/config/default.yaml index 3a2e560..70fe45c 100644 --- a/config/default.yaml +++ b/config/default.yaml @@ -209,6 +209,20 @@ health: # Defaults to 418. unhealthyStatus: 418 + # Sentry options. Sentry is a tool used to receive/collate/triage runtime + # errors and performance issues. Skip this section if you do not wish to use + # Sentry. + sentry: + # The key used to upload Sentry data to the server. + # dsn: "https://XXXXXXXXX@example.com/YYY + + # Frequency of performance monitoring. + # A number in [0.0, 1.0], where 0.0 means "don't bother with tracing" + # and 1.0 means "trace performance at every opportunity". + # tracesSampleRate: 0.5 + + + # Options for exposing web APIs. web: # Whether to enable web APIs. diff --git a/package.json b/package.json index 6992a4f..ea1705a 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,8 @@ "typescript-formatter": "^7.2" }, "dependencies": { + "@sentry/node": "^7.17.2", + "@sentry/tracing": "^7.17.2", "await-lock": "^2.2.2", "body-parser": "^1.20.1", "config": "^3.3.8", diff --git a/src/ManagementRoomOutput.ts b/src/ManagementRoomOutput.ts index e47ba61..9165900 100644 --- a/src/ManagementRoomOutput.ts +++ b/src/ManagementRoomOutput.ts @@ -14,6 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ +import * as Sentry from "@sentry/node"; import { extractRequestError, LogLevel, LogService, MatrixClient, MessageType, Permalinks, TextualMessageEventContent, UserID } from "matrix-bot-sdk"; import { IConfig } from "./config"; import { htmlEscape } from "./utils"; @@ -34,7 +35,7 @@ export default class ManagementRoomOutput { private readonly managementRoomId: string, private readonly client: MatrixClient, private readonly config: IConfig, - ) { + ) { } @@ -94,6 +95,9 @@ export default class ManagementRoomOutput { * @param isRecursive Whether logMessage is being called from logMessage. */ public async logMessage(level: LogLevel, module: string, message: string | any, additionalRoomIds: string[] | string | null = null, isRecursive = false): Promise { + if (level === LogLevel.ERROR) { + Sentry.captureMessage(`${module}: ${message}`, 'error'); + } if (!additionalRoomIds) additionalRoomIds = []; if (!Array.isArray(additionalRoomIds)) additionalRoomIds = [additionalRoomIds]; @@ -115,7 +119,12 @@ export default class ManagementRoomOutput { evContent = await this.replaceRoomIdsWithPills(clientMessage, new Set(roomIds), "m.notice"); } - await client.sendMessage(this.managementRoomId, evContent); + try { + await client.sendMessage(this.managementRoomId, evContent); + } catch (ex) { + // We want to be informed if we cannot log a message. + Sentry.captureException(ex); + } } levelToFn[level.toString()](module, message); diff --git a/src/Mjolnir.ts b/src/Mjolnir.ts index 304242c..cf6b461 100644 --- a/src/Mjolnir.ts +++ b/src/Mjolnir.ts @@ -319,11 +319,10 @@ export class Mjolnir { LogService.error("Mjolnir", extractRequestError(err)); this.stop(); await this.managementRoomOutput.logMessage(LogLevel.ERROR, "Mjolnir@startup", "Startup failed due to error - see console"); - throw err; } catch (e) { LogService.error("Mjolnir", `Failed to report startup error to the management room: ${e}`); - throw err; } + throw err; } } @@ -602,3 +601,4 @@ export class Mjolnir { } } } + diff --git a/src/config.ts b/src/config.ts index e62359a..75534a4 100644 --- a/src/config.ts +++ b/src/config.ts @@ -84,6 +84,16 @@ export interface IConfig { healthyStatus: number; unhealthyStatus: number; }; + // If specified, attempt to upload any crash statistics to sentry. + sentry?: { + dsn: string; + + // Frequency of performance monitoring. + // + // A number in [0.0, 1.0], where 0.0 means "don't bother with tracing" + // and 1.0 means "trace performance at every opportunity". + tracesSampleRate: number; + }; }; web: { enabled: boolean; diff --git a/src/index.ts b/src/index.ts index 38e72bb..fa271d2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -15,6 +15,9 @@ limitations under the License. */ import * as path from "path"; + +import { Healthz } from "./health/healthz"; + import { LogLevel, LogService, @@ -23,10 +26,10 @@ import { RichConsoleLogger, SimpleFsStorageProvider } from "matrix-bot-sdk"; + import { read as configRead } from "./config"; -import { Healthz } from "./health/healthz"; import { Mjolnir } from "./Mjolnir"; -import { patchMatrixClient } from "./utils"; +import { initializeSentry, patchMatrixClient } from "./utils"; (async function () { @@ -39,6 +42,10 @@ import { patchMatrixClient } from "./utils"; LogService.info("index", "Starting bot..."); + // Initialize error reporting as early as possible. + if (config.health.sentry) { + initializeSentry(config); + } const healthz = new Healthz(config); healthz.isHealthy = false; // start off unhealthy if (config.health.healthz.enabled) { diff --git a/src/utils.ts b/src/utils.ts index 07009c6..c2da851 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -24,7 +24,11 @@ import { } from "matrix-bot-sdk"; import { ClientRequest, IncomingMessage } from "http"; import { default as parseDuration } from "parse-duration"; +import * as Sentry from '@sentry/node'; +import * as _ from '@sentry/tracing'; // Performing the import activates tracing. + import ManagementRoomOutput from "./ManagementRoomOutput"; +import { IConfig } from "./config"; // Define a few aliases to simplify parsing durations. @@ -396,3 +400,27 @@ export function patchMatrixClient() { patchMatrixClientForConciseExceptions(); patchMatrixClientForRetry(); } + +/** + * Initialize Sentry for error monitoring and reporting. + * + * This method is idempotent. If `config` specifies that Sentry + * should not be used, it does nothing. + */ +export function initializeSentry(config: IConfig) { + if (sentryInitialized) { + return; + } + if (config.health.sentry) { + // Configure error monitoring with Sentry. + let sentry = config.health.sentry; + Sentry.init({ + dsn: sentry.dsn, + tracesSampleRate: sentry.tracesSampleRate, + }); + sentryInitialized = true; + } +} +// Set to `true` once we have initialized `Sentry` to ensure +// that we do not attempt to initialize it more than once. +let sentryInitialized = false; \ No newline at end of file diff --git a/test/integration/mjolnirSetupUtils.ts b/test/integration/mjolnirSetupUtils.ts index 93ce6b8..bdad3ac 100644 --- a/test/integration/mjolnirSetupUtils.ts +++ b/test/integration/mjolnirSetupUtils.ts @@ -23,7 +23,7 @@ import { } from "matrix-bot-sdk"; import { Mjolnir} from '../../src/Mjolnir'; import { overrideRatelimitForUser, registerUser } from "./clientHelper"; -import { patchMatrixClient } from "../../src/utils"; +import { initializeSentry, patchMatrixClient } from "../../src/utils"; import { IConfig } from "../../src/config"; /** @@ -49,6 +49,8 @@ export async function ensureAliasedRoomExists(client: MatrixClient, alias: strin } async function configureMjolnir(config: IConfig) { + // Initialize error monitoring as early as possible. + initializeSentry(config); try { await registerUser(config.homeserverUrl, config.pantalaimon.username, config.pantalaimon.username, config.pantalaimon.password, true) } catch (e) { diff --git a/yarn.lock b/yarn.lock index ee947b3..02d1874 100644 --- a/yarn.lock +++ b/yarn.lock @@ -104,6 +104,51 @@ domhandler "^4.2.0" selderee "^0.6.0" +"@sentry/core@7.22.0": + version "7.22.0" + resolved "https://registry.yarnpkg.com/@sentry/core/-/core-7.22.0.tgz#8e50f288e5e8fcaa2774daffd2487e042a392893" + integrity sha512-qYJiJrL1mfQQln84mNunBRUkXq7xDGQQoNh4Sz9VYP5698va51zmS5BLYRCZ5CkPwRYNuhUqlUXN7bpYGYOOIA== + dependencies: + "@sentry/types" "7.22.0" + "@sentry/utils" "7.22.0" + tslib "^1.9.3" + +"@sentry/node@^7.17.2": + version "7.22.0" + resolved "https://registry.yarnpkg.com/@sentry/node/-/node-7.22.0.tgz#d575481e56d3326ad457378db5ab7cc804b712fd" + integrity sha512-jKhxqKsbEEaY/g3FTzpj1fwukN0IkNv4V+0Fl+t/EmSNUL/7q5FMmDBa+fFQuQzwps8UEpzqPOzMSRapVsoP0w== + dependencies: + "@sentry/core" "7.22.0" + "@sentry/types" "7.22.0" + "@sentry/utils" "7.22.0" + cookie "^0.4.1" + https-proxy-agent "^5.0.0" + lru_map "^0.3.3" + tslib "^1.9.3" + +"@sentry/tracing@^7.17.2": + version "7.22.0" + resolved "https://registry.yarnpkg.com/@sentry/tracing/-/tracing-7.22.0.tgz#ec29325ee2c5959670097c104e47a78797cef17b" + integrity sha512-s68aSnrRaWQ+Z5oG9ozIegUkofZy9PwicuXYEPA8K/R30F1CVpHvDZ/3KlFnByl+aXTbAyLBQzN2sAObB5g4pQ== + dependencies: + "@sentry/core" "7.22.0" + "@sentry/types" "7.22.0" + "@sentry/utils" "7.22.0" + tslib "^1.9.3" + +"@sentry/types@7.22.0": + version "7.22.0" + resolved "https://registry.yarnpkg.com/@sentry/types/-/types-7.22.0.tgz#58e4ce77b523048e0f31e2ea4b597946d76f6079" + integrity sha512-LhCL+wb1Jch+OesB2CIt6xpfO1Ab6CRvoNYRRzVumWPLns1T3ZJkarYfhbLaOEIb38EIbPgREdxn2AJT560U4Q== + +"@sentry/utils@7.22.0": + version "7.22.0" + resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-7.22.0.tgz#fb46dc2761e2d44cf70bc3e1fba61d55852904b5" + integrity sha512-1GiNw1opIngxg0nktCTc9wibh4/LV12kclrnB9dAOHrqazZXHXZRAkjqrhQphKcMpT+3By91W6EofjaDt5a/hg== + dependencies: + "@sentry/types" "7.22.0" + tslib "^1.9.3" + "@tootallnate/once@1": version "1.1.2" resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82" @@ -829,6 +874,11 @@ cookie@0.5.0: resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.5.0.tgz#d1f5d71adec6558c58f389987c366aa47e994f8b" integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw== +cookie@^0.4.1: + version "0.4.2" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.2.tgz#0e41f24de5ecf317947c82fc789e06a884824432" + integrity sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA== + core-util-is@1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" @@ -2203,6 +2253,11 @@ lru-cache@^7.10.1: resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-7.13.1.tgz#267a81fbd0881327c46a81c5922606a2cfe336c4" integrity sha512-CHqbAq7NFlW3RSnoWXLJBxCWaZVBrfa9UEHId2M3AW8iEBurbqduNexEUCGc3SHc6iCYXNJCDi903LajSVAEPQ== +lru_map@^0.3.3: + version "0.3.3" + resolved "https://registry.yarnpkg.com/lru_map/-/lru_map-0.3.3.tgz#b5c8351b9464cbd750335a79650a0ec0e56118dd" + integrity sha512-Pn9cox5CsMYngeDbmChANltQl+5pi6XmTrraMSzhPmMBbmgcxmqWry0U3PGapCU1yB4/LqCcom7qhHZiF/jGfQ== + make-error@^1.1.1: version "1.3.6" resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" @@ -3436,9 +3491,9 @@ tsconfig-paths@^3.5.0: minimist "^1.2.0" strip-bom "^3.0.0" -tslib@^1.13.0, tslib@^1.8.1: +tslib@^1.13.0, tslib@^1.8.1, tslib@^1.9.3: version "1.14.1" - resolved "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== tslint@^6.1.3: