Skip to content

Commit

Permalink
feat: sentry integration (misskey-dev#13897)
Browse files Browse the repository at this point in the history
* wip

* wip

* wip

* wip

* Update CHANGELOG.md

* Update ApiCallService.ts

* Update config.ts
  • Loading branch information
syuilo authored May 28, 2024
1 parent 89b27d8 commit 80f3cb9
Show file tree
Hide file tree
Showing 10 changed files with 776 additions and 28 deletions.
19 changes: 17 additions & 2 deletions .config/docker_example.yml
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ redis:
# ┌───────────────────────────┐
#───┘ MeiliSearch configuration └─────────────────────────────

# You can set scope to local (default value) or global
# You can set scope to local (default value) or global
# (include notes from remote).

#meilisearch:
Expand Down Expand Up @@ -136,6 +136,21 @@ redis:

id: 'aidx'

# ┌────────────────┐
#───┘ Error tracking └──────────────────────────────────────────

# Sentry is available for error tracking.
# See the Sentry documentation for more details on options.

#sentryForBackend:
# enableNodeProfiling: true
# options:
# dsn: 'https://[email protected]/0'

#sentryForFrontend:
# options:
# dsn: 'https://[email protected]/0'

# ┌─────────────────────┐
#───┘ Other configuration └─────────────────────────────────────

Expand Down Expand Up @@ -185,7 +200,7 @@ proxyRemoteFiles: true
signToActivityPubGet: true

# For security reasons, uploading attachments from the intranet is prohibited,
# but exceptions can be made from the following settings. Default value is "undefined".
# but exceptions can be made from the following settings. Default value is "undefined".
# Read changelog to learn more (Improvements of 12.90.0 (2021/09/04)).
#allowedPrivateNetworks: [
# '127.0.0.1/32'
Expand Down
15 changes: 15 additions & 0 deletions .config/example.yml
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,21 @@ redis:

id: 'aidx'

# ┌────────────────┐
#───┘ Error tracking └──────────────────────────────────────────

# Sentry is available for error tracking.
# See the Sentry documentation for more details on options.

#sentryForBackend:
# enableNodeProfiling: true
# options:
# dsn: 'https://[email protected]/0'

#sentryForFrontend:
# options:
# dsn: 'https://[email protected]/0'

# ┌─────────────────────┐
#───┘ Other configuration └─────────────────────────────────────

Expand Down
15 changes: 15 additions & 0 deletions .devcontainer/devcontainer.yml
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,21 @@ redis:

id: 'aidx'

# ┌────────────────┐
#───┘ Error tracking └──────────────────────────────────────────

# Sentry is available for error tracking.
# See the Sentry documentation for more details on options.

#sentryForBackend:
# enableNodeProfiling: true
# options:
# dsn: 'https://[email protected]/0'

#sentryForFrontend:
# options:
# dsn: 'https://[email protected]/0'

# ┌─────────────────────┐
#───┘ Other configuration └─────────────────────────────────────

Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
- 管理者向け権限 `read:admin:show-users``read:admin:show-user` に統合されました。必要に応じてAPIトークンを再発行してください。

### General
- Feat: エラートラッキングにSentryを使用できるようになりました
- Enhance: URLプレビューの有効化・無効化を設定できるように #13569
- Enhance: アンテナでBotによるノートを除外できるように
(Cherry-picked from https://github.com/MisskeyIO/misskey/pull/545)
Expand Down
16 changes: 16 additions & 0 deletions chart/files/default.yml
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,22 @@ redis:
# ID SETTINGS AFTER THAT!

id: "aidx"

# ┌────────────────┐
#───┘ Error tracking └──────────────────────────────────────────

# Sentry is available for error tracking.
# See the Sentry documentation for more details on options.

#sentryForBackend:
# enableNodeProfiling: true
# options:
# dsn: 'https://[email protected]/0'

#sentryForFrontend:
# options:
# dsn: 'https://[email protected]/0'

# ┌─────────────────────┐
#───┘ Other configuration └─────────────────────────────────────

Expand Down
4 changes: 3 additions & 1 deletion packages/backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"private": true,
"type": "module",
"engines": {
"node": ">=20.10.0"
"node": "^20.10.0"
},
"scripts": {
"start": "node ./built/boot/entry.js",
Expand Down Expand Up @@ -86,6 +86,8 @@
"@nestjs/core": "10.3.8",
"@nestjs/testing": "10.3.8",
"@peertube/http-signature": "1.7.0",
"@sentry/node": "^8.5.0",
"@sentry/profiling-node": "^8.5.0",
"@simplewebauthn/server": "10.0.0",
"@sinonjs/fake-timers": "11.2.2",
"@smithy/node-http-handler": "2.5.0",
Expand Down
20 changes: 20 additions & 0 deletions packages/backend/src/boot/master.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import * as os from 'node:os';
import cluster from 'node:cluster';
import chalk from 'chalk';
import chalkTemplate from 'chalk-template';
import * as Sentry from '@sentry/node';
import { nodeProfilingIntegration } from '@sentry/profiling-node';
import Logger from '@/logger.js';
import { loadConfig } from '@/config.js';
import type { Config } from '@/config.js';
Expand Down Expand Up @@ -71,6 +73,24 @@ export async function masterMain() {

bootLogger.succ('Misskey initialized');

if (config.sentryForBackend) {
Sentry.init({
integrations: [
...(config.sentryForBackend.enableNodeProfiling ? [nodeProfilingIntegration()] : []),
],

// Performance Monitoring
tracesSampleRate: 1.0, // Capture 100% of the transactions

// Set sampling rate for profiling - this is relative to tracesSampleRate
profilesSampleRate: 1.0,

maxBreadcrumbs: 0,

...config.sentryForBackend.options,
});
}

if (envOption.disableClustering) {
if (envOption.onlyServer) {
await server();
Expand Down
7 changes: 7 additions & 0 deletions packages/backend/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import * as fs from 'node:fs';
import { fileURLToPath } from 'node:url';
import { dirname, resolve } from 'node:path';
import * as yaml from 'js-yaml';
import * as Sentry from '@sentry/node';
import type { RedisOptions } from 'ioredis';

type RedisOptionsSource = Partial<RedisOptions> & {
Expand Down Expand Up @@ -56,6 +57,8 @@ type Source = {
index: string;
scope?: 'local' | 'global' | string[];
};
sentryForBackend?: { options: Partial<Sentry.NodeOptions>; enableNodeProfiling: boolean; };
sentryForFrontend?: { options: Partial<Sentry.NodeOptions> };

publishTarballInsteadOfProvideRepositoryUrl?: boolean;

Expand Down Expand Up @@ -166,6 +169,8 @@ export type Config = {
redisForPubsub: RedisOptions & RedisOptionsSource;
redisForJobQueue: RedisOptions & RedisOptionsSource;
redisForTimelines: RedisOptions & RedisOptionsSource;
sentryForBackend: { options: Partial<Sentry.NodeOptions>; enableNodeProfiling: boolean; } | undefined;
sentryForFrontend: { options: Partial<Sentry.NodeOptions> } | undefined;
perChannelMaxNoteCacheCount: number;
perUserNotificationsMaxCount: number;
deactivateAntennaThreshold: number;
Expand Down Expand Up @@ -234,6 +239,8 @@ export function loadConfig(): Config {
redisForPubsub: config.redisForPubsub ? convertRedisOptions(config.redisForPubsub, host) : redis,
redisForJobQueue: config.redisForJobQueue ? convertRedisOptions(config.redisForJobQueue, host) : redis,
redisForTimelines: config.redisForTimelines ? convertRedisOptions(config.redisForTimelines, host) : redis,
sentryForBackend: config.sentryForBackend,
sentryForFrontend: config.sentryForFrontend,
id: config.id,
proxy: config.proxy,
proxySmtp: config.proxySmtp,
Expand Down
77 changes: 52 additions & 25 deletions packages/backend/src/server/api/ApiCallService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { randomUUID } from 'node:crypto';
import * as fs from 'node:fs';
import * as stream from 'node:stream/promises';
import { Inject, Injectable } from '@nestjs/common';
import * as Sentry from '@sentry/node';
import { DI } from '@/di-symbols.js';
import { getIpHash } from '@/misc/get-ip-hash.js';
import type { MiLocalUser, MiUser } from '@/models/User.js';
Expand All @@ -17,6 +18,7 @@ import { MetaService } from '@/core/MetaService.js';
import { createTemp } from '@/misc/create-temp.js';
import { bindThis } from '@/decorators.js';
import { RoleService } from '@/core/RoleService.js';
import type { Config } from '@/config.js';
import { ApiError } from './error.js';
import { RateLimiterService } from './RateLimiterService.js';
import { ApiLoggerService } from './ApiLoggerService.js';
Expand All @@ -38,6 +40,9 @@ export class ApiCallService implements OnApplicationShutdown {
private userIpHistoriesClearIntervalId: NodeJS.Timeout;

constructor(
@Inject(DI.config)
private config: Config,

@Inject(DI.userIpsRepository)
private userIpsRepository: UserIpsRepository,

Expand Down Expand Up @@ -88,6 +93,48 @@ export class ApiCallService implements OnApplicationShutdown {
}
}

#onExecError(ep: IEndpoint, data: any, err: Error): void {
if (err instanceof ApiError || err instanceof AuthenticationError) {
throw err;
} else {
const errId = randomUUID();
this.logger.error(`Internal error occurred in ${ep.name}: ${err.message}`, {
ep: ep.name,
ps: data,
e: {
message: err.message,
code: err.name,
stack: err.stack,
id: errId,
},
});
console.error(err, errId);

if (this.config.sentryForBackend) {
Sentry.captureMessage(`Internal error occurred in ${ep.name}: ${err.message}`, {
extra: {
ep: ep.name,
ps: data,
e: {
message: err.message,
code: err.name,
stack: err.stack,
id: errId,
},
},
});
}

throw new ApiError(null, {
e: {
message: err.message,
code: err.name,
id: errId,
},
});
}
}

@bindThis
public handleRequest(
endpoint: IEndpoint & { exec: any },
Expand Down Expand Up @@ -362,31 +409,11 @@ export class ApiCallService implements OnApplicationShutdown {
}

// API invoking
return await ep.exec(data, user, token, file, request.ip, request.headers).catch((err: Error) => {
if (err instanceof ApiError || err instanceof AuthenticationError) {
throw err;
} else {
const errId = randomUUID();
this.logger.error(`Internal error occurred in ${ep.name}: ${err.message}`, {
ep: ep.name,
ps: data,
e: {
message: err.message,
code: err.name,
stack: err.stack,
id: errId,
},
});
console.error(err, errId);
throw new ApiError(null, {
e: {
message: err.message,
code: err.name,
id: errId,
},
});
}
});
if (this.config.sentryForBackend) {
return await Sentry.startSpan({ name: 'API: ' + ep.name }, () => ep.exec(data, user, token, file, request.ip, request.headers).catch((err: Error) => this.#onExecError(ep, data, err)));
} else {
return await ep.exec(data, user, token, file, request.ip, request.headers).catch((err: Error) => this.#onExecError(ep, data, err));
}
}

@bindThis
Expand Down
Loading

0 comments on commit 80f3cb9

Please sign in to comment.