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`.
This commit is contained in:
David Teller 2022-10-31 13:09:10 +01:00
parent e35b855744
commit 27ab1c710f
9 changed files with 136 additions and 9 deletions

View File

@ -209,6 +209,20 @@ health:
# Defaults to 418. # Defaults to 418.
unhealthyStatus: 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. # Options for exposing web APIs.
web: web:
# Whether to enable web APIs. # Whether to enable web APIs.

View File

@ -44,6 +44,8 @@
"typescript-formatter": "^7.2" "typescript-formatter": "^7.2"
}, },
"dependencies": { "dependencies": {
"@sentry/node": "^7.17.2",
"@sentry/tracing": "^7.17.2",
"await-lock": "^2.2.2", "await-lock": "^2.2.2",
"body-parser": "^1.20.1", "body-parser": "^1.20.1",
"config": "^3.3.8", "config": "^3.3.8",

View File

@ -14,6 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import * as Sentry from "@sentry/node";
import { extractRequestError, LogLevel, LogService, MatrixClient, MessageType, Permalinks, TextualMessageEventContent, UserID } from "matrix-bot-sdk"; import { extractRequestError, LogLevel, LogService, MatrixClient, MessageType, Permalinks, TextualMessageEventContent, UserID } from "matrix-bot-sdk";
import { IConfig } from "./config"; import { IConfig } from "./config";
import { htmlEscape } from "./utils"; import { htmlEscape } from "./utils";
@ -34,7 +35,7 @@ export default class ManagementRoomOutput {
private readonly managementRoomId: string, private readonly managementRoomId: string,
private readonly client: MatrixClient, private readonly client: MatrixClient,
private readonly config: IConfig, private readonly config: IConfig,
) { ) {
} }
@ -94,6 +95,9 @@ export default class ManagementRoomOutput {
* @param isRecursive Whether logMessage is being called from logMessage. * @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<any> { public async logMessage(level: LogLevel, module: string, message: string | any, additionalRoomIds: string[] | string | null = null, isRecursive = false): Promise<any> {
if (level === LogLevel.ERROR) {
Sentry.captureMessage(`${module}: ${message}`, 'error');
}
if (!additionalRoomIds) additionalRoomIds = []; if (!additionalRoomIds) additionalRoomIds = [];
if (!Array.isArray(additionalRoomIds)) 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"); 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); levelToFn[level.toString()](module, message);

View File

@ -319,11 +319,10 @@ export class Mjolnir {
LogService.error("Mjolnir", extractRequestError(err)); LogService.error("Mjolnir", extractRequestError(err));
this.stop(); this.stop();
await this.managementRoomOutput.logMessage(LogLevel.ERROR, "Mjolnir@startup", "Startup failed due to error - see console"); await this.managementRoomOutput.logMessage(LogLevel.ERROR, "Mjolnir@startup", "Startup failed due to error - see console");
throw err;
} catch (e) { } catch (e) {
LogService.error("Mjolnir", `Failed to report startup error to the management room: ${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 {
} }
} }
} }

View File

@ -84,6 +84,16 @@ export interface IConfig {
healthyStatus: number; healthyStatus: number;
unhealthyStatus: 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: { web: {
enabled: boolean; enabled: boolean;

View File

@ -15,6 +15,9 @@ limitations under the License.
*/ */
import * as path from "path"; import * as path from "path";
import { Healthz } from "./health/healthz";
import { import {
LogLevel, LogLevel,
LogService, LogService,
@ -23,10 +26,10 @@ import {
RichConsoleLogger, RichConsoleLogger,
SimpleFsStorageProvider SimpleFsStorageProvider
} from "matrix-bot-sdk"; } from "matrix-bot-sdk";
import { read as configRead } from "./config"; import { read as configRead } from "./config";
import { Healthz } from "./health/healthz";
import { Mjolnir } from "./Mjolnir"; import { Mjolnir } from "./Mjolnir";
import { patchMatrixClient } from "./utils"; import { initializeSentry, patchMatrixClient } from "./utils";
(async function () { (async function () {
@ -39,6 +42,10 @@ import { patchMatrixClient } from "./utils";
LogService.info("index", "Starting bot..."); LogService.info("index", "Starting bot...");
// Initialize error reporting as early as possible.
if (config.health.sentry) {
initializeSentry(config);
}
const healthz = new Healthz(config); const healthz = new Healthz(config);
healthz.isHealthy = false; // start off unhealthy healthz.isHealthy = false; // start off unhealthy
if (config.health.healthz.enabled) { if (config.health.healthz.enabled) {

View File

@ -24,7 +24,11 @@ import {
} from "matrix-bot-sdk"; } from "matrix-bot-sdk";
import { ClientRequest, IncomingMessage } from "http"; import { ClientRequest, IncomingMessage } from "http";
import { default as parseDuration } from "parse-duration"; 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 ManagementRoomOutput from "./ManagementRoomOutput";
import { IConfig } from "./config";
// Define a few aliases to simplify parsing durations. // Define a few aliases to simplify parsing durations.
@ -396,3 +400,27 @@ export function patchMatrixClient() {
patchMatrixClientForConciseExceptions(); patchMatrixClientForConciseExceptions();
patchMatrixClientForRetry(); 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;

View File

@ -23,7 +23,7 @@ import {
} from "matrix-bot-sdk"; } from "matrix-bot-sdk";
import { Mjolnir} from '../../src/Mjolnir'; import { Mjolnir} from '../../src/Mjolnir';
import { overrideRatelimitForUser, registerUser } from "./clientHelper"; import { overrideRatelimitForUser, registerUser } from "./clientHelper";
import { patchMatrixClient } from "../../src/utils"; import { initializeSentry, patchMatrixClient } from "../../src/utils";
import { IConfig } from "../../src/config"; import { IConfig } from "../../src/config";
/** /**
@ -49,6 +49,8 @@ export async function ensureAliasedRoomExists(client: MatrixClient, alias: strin
} }
async function configureMjolnir(config: IConfig) { async function configureMjolnir(config: IConfig) {
// Initialize error monitoring as early as possible.
initializeSentry(config);
try { try {
await registerUser(config.homeserverUrl, config.pantalaimon.username, config.pantalaimon.username, config.pantalaimon.password, true) await registerUser(config.homeserverUrl, config.pantalaimon.username, config.pantalaimon.username, config.pantalaimon.password, true)
} catch (e) { } catch (e) {

View File

@ -104,6 +104,51 @@
domhandler "^4.2.0" domhandler "^4.2.0"
selderee "^0.6.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": "@tootallnate/once@1":
version "1.1.2" version "1.1.2"
resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82" 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" resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.5.0.tgz#d1f5d71adec6558c58f389987c366aa47e994f8b"
integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw== 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: core-util-is@1.0.2:
version "1.0.2" version "1.0.2"
resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" 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" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-7.13.1.tgz#267a81fbd0881327c46a81c5922606a2cfe336c4"
integrity sha512-CHqbAq7NFlW3RSnoWXLJBxCWaZVBrfa9UEHId2M3AW8iEBurbqduNexEUCGc3SHc6iCYXNJCDi903LajSVAEPQ== 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: make-error@^1.1.1:
version "1.3.6" version "1.3.6"
resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" 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" minimist "^1.2.0"
strip-bom "^3.0.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" 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== integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
tslint@^6.1.3: tslint@^6.1.3: