diff --git a/packages/backend/src/boot/master.ts b/packages/backend/src/boot/master.ts index 533ebe5bbb..aac68cb02f 100644 --- a/packages/backend/src/boot/master.ts +++ b/packages/backend/src/boot/master.ts @@ -13,6 +13,7 @@ import { loadConfig } from '@/config.js'; import type { Config } from '@/config.js'; import { showMachineInfo } from '@/misc/show-machine-info.js'; import { envOption } from '@/env.js'; +import { initTelemetry } from '@/core/telemetry/telemetry-registry.js'; import { initExtraThreadPool, jobQueue, server } from './common.js'; const logger = new Logger('core', 'cyan'); @@ -66,26 +67,7 @@ export async function masterMain() { initExtraThreadPool(config); - if (config.sentryForBackend) { - const Sentry = await import('@sentry/node'); - const { nodeProfilingIntegration } = await import('@sentry/profiling-node'); - - 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, - }); - } + await initTelemetry(config); bootLogger.info( `mode: [disableClustering: ${envOption.disableClustering}, onlyServer: ${envOption.onlyServer}, onlyQueue: ${envOption.onlyQueue}]`, diff --git a/packages/backend/src/boot/worker.ts b/packages/backend/src/boot/worker.ts index d8fb1eeaac..00d5dd9063 100644 --- a/packages/backend/src/boot/worker.ts +++ b/packages/backend/src/boot/worker.ts @@ -6,6 +6,7 @@ import cluster from 'node:cluster'; import { envOption } from '@/env.js'; import { loadConfig } from '@/config.js'; +import { initTelemetry } from '@/core/telemetry/telemetry-registry.js'; import { initExtraThreadPool, jobQueue, server } from './common.js'; /** @@ -16,26 +17,7 @@ export async function workerMain() { initExtraThreadPool(config); - if (config.sentryForBackend) { - const Sentry = await import('@sentry/node'); - const { nodeProfilingIntegration } = await import('@sentry/profiling-node'); - - 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, - }); - } + await initTelemetry(config); if (envOption.onlyServer) { await server(); diff --git a/packages/backend/src/core/CoreModule.ts b/packages/backend/src/core/CoreModule.ts index f075671d93..6ac4eb9079 100644 --- a/packages/backend/src/core/CoreModule.ts +++ b/packages/backend/src/core/CoreModule.ts @@ -154,10 +154,12 @@ import { ApQuestionService } from './activitypub/models/ApQuestionService.js'; import { QueueModule } from './QueueModule.js'; import { QueueService } from './QueueService.js'; import { LoggerService } from './LoggerService.js'; +import { TelemetryService } from './telemetry/TelemetryService.js'; import type { Provider } from '@nestjs/common'; //#region 文字列ベースでのinjection用(循環参照対応のため) const $LoggerService: Provider = { provide: 'LoggerService', useExisting: LoggerService }; +const $TelemetryService: Provider = { provide: 'TelemetryService', useExisting: TelemetryService }; const $AbuseReportService: Provider = { provide: 'AbuseReportService', useExisting: AbuseReportService }; const $AbuseReportNotificationService: Provider = { provide: 'AbuseReportNotificationService', useExisting: AbuseReportNotificationService }; const $AccountMoveService: Provider = { provide: 'AccountMoveService', useExisting: AccountMoveService }; @@ -458,6 +460,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting ApPersonService, ApQuestionService, QueueService, + TelemetryService, //#region 文字列ベースでのinjection用(循環参照対応のため) $LoggerService, @@ -606,6 +609,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting $ApNoteService, $ApPersonService, $ApQuestionService, + $TelemetryService, //#endregion ], exports: [ @@ -757,6 +761,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting ApPersonService, ApQuestionService, QueueService, + TelemetryService, //#region 文字列ベースでのinjection用(循環参照対応のため) $LoggerService, @@ -903,6 +908,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting $ApNoteService, $ApPersonService, $ApQuestionService, + $TelemetryService, //#endregion ], }) diff --git a/packages/backend/src/core/telemetry/TelemetryService.ts b/packages/backend/src/core/telemetry/TelemetryService.ts new file mode 100644 index 0000000000..e73beb4f8a --- /dev/null +++ b/packages/backend/src/core/telemetry/TelemetryService.ts @@ -0,0 +1,28 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Injectable } from '@nestjs/common'; +import { bindThis } from '@/decorators.js'; +import { captureMessage, shutdownTelemetry, startSpan } from './telemetry-registry.js'; +import type { OnApplicationShutdown } from '@nestjs/common'; +import type { TelemetryCaptureMessageOptions } from './adapters/TelemetryAdapter.js'; + +@Injectable() +export class TelemetryService implements OnApplicationShutdown { + @bindThis + public captureMessage(message: string, opts: TelemetryCaptureMessageOptions): void { + captureMessage(message, opts); + } + + @bindThis + public startSpan(name: string, fn: () => T): T { + return startSpan(name, fn); + } + + @bindThis + public async onApplicationShutdown(_signal?: string): Promise { + await shutdownTelemetry(); + } +} diff --git a/packages/backend/src/core/telemetry/adapters/SentryTelemetryAdapter.ts b/packages/backend/src/core/telemetry/adapters/SentryTelemetryAdapter.ts new file mode 100644 index 0000000000..c155c9d74c --- /dev/null +++ b/packages/backend/src/core/telemetry/adapters/SentryTelemetryAdapter.ts @@ -0,0 +1,53 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import type { Config } from '@/config.js'; +import type { TelemetryAdapter, TelemetryCaptureMessageOptions } from './TelemetryAdapter.js'; + +export class SentryTelemetryAdapter implements TelemetryAdapter { + private constructor( + private readonly Sentry: typeof import('@sentry/node'), + ) { + } + + public static async create(config: NonNullable): Promise { + const Sentry = await import('@sentry/node'); + const { nodeProfilingIntegration } = await import('@sentry/profiling-node'); + + Sentry.init({ + integrations: [ + ...(config.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.options, + }); + + return new SentryTelemetryAdapter(Sentry); + } + + public captureMessage(message: string, opts: TelemetryCaptureMessageOptions): void { + this.Sentry.captureMessage(message, { + level: opts.level, + ...(opts.userId != null ? { user: { id: opts.userId } } : {}), + extra: opts.extra, + }); + } + + public startSpan(name: string, fn: () => T): T { + return this.Sentry.startSpan({ name }, fn); + } + + public async shutdown(): Promise { + await this.Sentry.close(); + } +} diff --git a/packages/backend/src/core/telemetry/adapters/TelemetryAdapter.ts b/packages/backend/src/core/telemetry/adapters/TelemetryAdapter.ts new file mode 100644 index 0000000000..d074de056d --- /dev/null +++ b/packages/backend/src/core/telemetry/adapters/TelemetryAdapter.ts @@ -0,0 +1,21 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export interface TelemetryCaptureMessageOptions { + level: 'error'; + userId?: string; + extra?: Record; +} + +/** + * Sentry・OpenTelemetryなど、エラートラッキング/APMサービスごとの実装差異を隠蔽するための抽象。 + * 新しいサービスを追加する場合はこのインターフェースを実装するアダプタをこのディレクトリに追加し、 + * telemetry-registry.tsのinitTelemetry内で登録する。 + */ +export interface TelemetryAdapter { + captureMessage(message: string, opts: TelemetryCaptureMessageOptions): void; + startSpan(name: string, fn: () => T): T; + shutdown(): Promise; +} diff --git a/packages/backend/src/core/telemetry/telemetry-registry.ts b/packages/backend/src/core/telemetry/telemetry-registry.ts new file mode 100644 index 0000000000..90dc267837 --- /dev/null +++ b/packages/backend/src/core/telemetry/telemetry-registry.ts @@ -0,0 +1,39 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import type { Config } from '@/config.js'; +import { SentryTelemetryAdapter } from './adapters/SentryTelemetryAdapter.js'; +import type { TelemetryAdapter, TelemetryCaptureMessageOptions } from './adapters/TelemetryAdapter.js'; + +/** + * NestのDIコンテナが構築される前(boot処理内)で初期化する必要があるため、 + * DIを介さないモジュールレベルの状態として有効なアダプタを保持する。 + * TelemetryServiceはこの状態への薄いラッパーとして振る舞う。 + */ +const adapters: TelemetryAdapter[] = []; + +export async function initTelemetry(config: Config): Promise { + if (config.sentryForBackend) { + adapters.push(await SentryTelemetryAdapter.create(config.sentryForBackend)); + } +} + +export function captureMessage(message: string, opts: TelemetryCaptureMessageOptions): void { + for (const adapter of adapters) { + adapter.captureMessage(message, opts); + } +} + +export function startSpan(name: string, fn: () => T): T { + const wrapped = adapters.reduceRight<() => T>( + (inner, adapter) => () => adapter.startSpan(name, inner), + fn, + ); + return wrapped(); +} + +export async function shutdownTelemetry(): Promise { + await Promise.all(adapters.map(adapter => adapter.shutdown())); +} diff --git a/packages/backend/src/queue/QueueProcessorService.ts b/packages/backend/src/queue/QueueProcessorService.ts index 2b3b3fc0ad..cd1e752fd5 100644 --- a/packages/backend/src/queue/QueueProcessorService.ts +++ b/packages/backend/src/queue/QueueProcessorService.ts @@ -9,6 +9,7 @@ import type { Config } from '@/config.js'; import { DI } from '@/di-symbols.js'; import type Logger from '@/logger.js'; import { bindThis } from '@/decorators.js'; +import { TelemetryService } from '@/core/telemetry/TelemetryService.js'; import { CheckModeratorsActivityProcessorService } from '@/queue/processors/CheckModeratorsActivityProcessorService.js'; import { UserWebhookDeliverProcessorService } from './processors/UserWebhookDeliverProcessorService.js'; import { SystemWebhookDeliverProcessorService } from './processors/SystemWebhookDeliverProcessorService.js'; @@ -92,6 +93,7 @@ export class QueueProcessorService implements OnApplicationShutdown { private config: Config, private queueLoggerService: QueueLoggerService, + private telemetryService: TelemetryService, private userWebhookDeliverProcessorService: UserWebhookDeliverProcessorService, private systemWebhookDeliverProcessorService: SystemWebhookDeliverProcessorService, private endedPollNotificationProcessorService: EndedPollNotificationProcessorService, @@ -156,13 +158,6 @@ export class QueueProcessorService implements OnApplicationShutdown { }; } - let Sentry: typeof import('@sentry/node') | undefined; - if (this.config.sentryForBackend) { - import('@sentry/node').then((mod) => { - Sentry = mod; - }); - } - //#region system { const processer = (job: Bull.Job) => { @@ -181,11 +176,7 @@ export class QueueProcessorService implements OnApplicationShutdown { }; this.systemQueueWorker = new Bull.Worker(QUEUE.SYSTEM, (job) => { - if (Sentry != null) { - return Sentry.startSpan({ name: 'Queue: System: ' + job.name }, () => processer(job)); - } else { - return processer(job); - } + return this.telemetryService.startSpan('Queue: System: ' + job.name, () => processer(job)); }, { ...baseWorkerOptions(this.config, QUEUE.SYSTEM), autorun: false, @@ -198,12 +189,10 @@ export class QueueProcessorService implements OnApplicationShutdown { .on('completed', (job, result) => logger.debug(`completed(${result}) id=${job.id}`)) .on('failed', (job, err: Error) => { logger.error(`failed(${err.name}: ${err.message}) id=${job?.id ?? '?'}`, { job: renderJob(job), e: renderError(err) }); - if (Sentry != null) { - Sentry.captureMessage(`Queue: System: ${job?.name ?? '?'}: ${err.name}: ${err.message}`, { - level: 'error', - extra: { job, err }, - }); - } + this.telemetryService.captureMessage(`Queue: System: ${job?.name ?? '?'}: ${err.name}: ${err.message}`, { + level: 'error', + extra: { job, err }, + }); }) .on('error', (err: Error) => logger.error(`error ${err.name}: ${err.message}`, { e: renderError(err) })) .on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`)); @@ -238,11 +227,7 @@ export class QueueProcessorService implements OnApplicationShutdown { }; this.dbQueueWorker = new Bull.Worker(QUEUE.DB, (job) => { - if (Sentry != null) { - return Sentry.startSpan({ name: 'Queue: DB: ' + job.name }, () => processer(job)); - } else { - return processer(job); - } + return this.telemetryService.startSpan('Queue: DB: ' + job.name, () => processer(job)); }, { ...baseWorkerOptions(this.config, QUEUE.DB), autorun: false, @@ -255,12 +240,10 @@ export class QueueProcessorService implements OnApplicationShutdown { .on('completed', (job, result) => logger.debug(`completed(${result}) id=${job.id}`)) .on('failed', (job, err) => { logger.error(`failed(${err.name}: ${err.message}) id=${job?.id ?? '?'}`, { job: renderJob(job), e: renderError(err) }); - if (Sentry != null) { - Sentry.captureMessage(`Queue: DB: ${job?.name ?? '?'}: ${err.name}: ${err.message}`, { - level: 'error', - extra: { job, err }, - }); - } + this.telemetryService.captureMessage(`Queue: DB: ${job?.name ?? '?'}: ${err.name}: ${err.message}`, { + level: 'error', + extra: { job, err }, + }); }) .on('error', (err: Error) => logger.error(`error ${err.name}: ${err.message}`, { e: renderError(err) })) .on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`)); @@ -270,11 +253,7 @@ export class QueueProcessorService implements OnApplicationShutdown { //#region deliver { this.deliverQueueWorker = new Bull.Worker(QUEUE.DELIVER, (job) => { - if (Sentry != null) { - return Sentry.startSpan({ name: 'Queue: Deliver' }, () => this.deliverProcessorService.process(job)); - } else { - return this.deliverProcessorService.process(job); - } + return this.telemetryService.startSpan('Queue: Deliver', () => this.deliverProcessorService.process(job)); }, { ...baseWorkerOptions(this.config, QUEUE.DELIVER), autorun: false, @@ -295,12 +274,10 @@ export class QueueProcessorService implements OnApplicationShutdown { .on('completed', (job, result) => logger.debug(`completed(${result}) ${getJobInfo(job, true)} to=${job.data.to}`)) .on('failed', (job, err) => { logger.error(`failed(${err.name}: ${err.message}) ${getJobInfo(job)} to=${job ? job.data.to : '-'}`); - if (Sentry != null) { - Sentry.captureMessage(`Queue: Deliver: ${err.name}: ${err.message}`, { - level: 'error', - extra: { job, err }, - }); - } + this.telemetryService.captureMessage(`Queue: Deliver: ${err.name}: ${err.message}`, { + level: 'error', + extra: { job, err }, + }); }) .on('error', (err: Error) => logger.error(`error ${err.name}: ${err.message}`, { e: renderError(err) })) .on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`)); @@ -310,11 +287,7 @@ export class QueueProcessorService implements OnApplicationShutdown { //#region inbox { this.inboxQueueWorker = new Bull.Worker(QUEUE.INBOX, (job) => { - if (Sentry != null) { - return Sentry.startSpan({ name: 'Queue: Inbox' }, () => this.inboxProcessorService.process(job)); - } else { - return this.inboxProcessorService.process(job); - } + return this.telemetryService.startSpan('Queue: Inbox', () => this.inboxProcessorService.process(job)); }, { ...baseWorkerOptions(this.config, QUEUE.INBOX), autorun: false, @@ -335,12 +308,10 @@ export class QueueProcessorService implements OnApplicationShutdown { .on('completed', (job, result) => logger.debug(`completed(${result}) ${getJobInfo(job, true)}`)) .on('failed', (job, err) => { logger.error(`failed(${err.name}: ${err.message}) ${getJobInfo(job)} activity=${job ? (job.data.activity ? job.data.activity.id : 'none') : '-'}`, { job: renderJob(job), e: renderError(err) }); - if (Sentry != null) { - Sentry.captureMessage(`Queue: Inbox: ${err.name}: ${err.message}`, { - level: 'error', - extra: { job, err }, - }); - } + this.telemetryService.captureMessage(`Queue: Inbox: ${err.name}: ${err.message}`, { + level: 'error', + extra: { job, err }, + }); }) .on('error', (err: Error) => logger.error(`error ${err.name}: ${err.message}`, { e: renderError(err) })) .on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`)); @@ -350,11 +321,7 @@ export class QueueProcessorService implements OnApplicationShutdown { //#region user-webhook deliver { this.userWebhookDeliverQueueWorker = new Bull.Worker(QUEUE.USER_WEBHOOK_DELIVER, (job) => { - if (Sentry != null) { - return Sentry.startSpan({ name: 'Queue: UserWebhookDeliver' }, () => this.userWebhookDeliverProcessorService.process(job)); - } else { - return this.userWebhookDeliverProcessorService.process(job); - } + return this.telemetryService.startSpan('Queue: UserWebhookDeliver', () => this.userWebhookDeliverProcessorService.process(job)); }, { ...baseWorkerOptions(this.config, QUEUE.USER_WEBHOOK_DELIVER), autorun: false, @@ -375,12 +342,10 @@ export class QueueProcessorService implements OnApplicationShutdown { .on('completed', (job, result) => logger.debug(`completed(${result}) ${getJobInfo(job, true)} to=${job.data.to}`)) .on('failed', (job, err) => { logger.error(`failed(${err.name}: ${err.message}) ${getJobInfo(job)} to=${job ? job.data.to : '-'}`); - if (Sentry != null) { - Sentry.captureMessage(`Queue: UserWebhookDeliver: ${err.name}: ${err.message}`, { - level: 'error', - extra: { job, err }, - }); - } + this.telemetryService.captureMessage(`Queue: UserWebhookDeliver: ${err.name}: ${err.message}`, { + level: 'error', + extra: { job, err }, + }); }) .on('error', (err: Error) => logger.error(`error ${err.name}: ${err.message}`, { e: renderError(err) })) .on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`)); @@ -390,11 +355,7 @@ export class QueueProcessorService implements OnApplicationShutdown { //#region system-webhook deliver { this.systemWebhookDeliverQueueWorker = new Bull.Worker(QUEUE.SYSTEM_WEBHOOK_DELIVER, (job) => { - if (Sentry != null) { - return Sentry.startSpan({ name: 'Queue: SystemWebhookDeliver' }, () => this.systemWebhookDeliverProcessorService.process(job)); - } else { - return this.systemWebhookDeliverProcessorService.process(job); - } + return this.telemetryService.startSpan('Queue: SystemWebhookDeliver', () => this.systemWebhookDeliverProcessorService.process(job)); }, { ...baseWorkerOptions(this.config, QUEUE.SYSTEM_WEBHOOK_DELIVER), autorun: false, @@ -415,12 +376,10 @@ export class QueueProcessorService implements OnApplicationShutdown { .on('completed', (job, result) => logger.debug(`completed(${result}) ${getJobInfo(job, true)} to=${job.data.to}`)) .on('failed', (job, err) => { logger.error(`failed(${err.name}: ${err.message}) ${getJobInfo(job)} to=${job ? job.data.to : '-'}`); - if (Sentry != null) { - Sentry.captureMessage(`Queue: SystemWebhookDeliver: ${err.name}: ${err.message}`, { - level: 'error', - extra: { job, err }, - }); - } + this.telemetryService.captureMessage(`Queue: SystemWebhookDeliver: ${err.name}: ${err.message}`, { + level: 'error', + extra: { job, err }, + }); }) .on('error', (err: Error) => logger.error(`error ${err.name}: ${err.message}`, { e: renderError(err) })) .on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`)); @@ -440,11 +399,7 @@ export class QueueProcessorService implements OnApplicationShutdown { }; this.relationshipQueueWorker = new Bull.Worker(QUEUE.RELATIONSHIP, (job) => { - if (Sentry != null) { - return Sentry.startSpan({ name: 'Queue: Relationship: ' + job.name }, () => processer(job)); - } else { - return processer(job); - } + return this.telemetryService.startSpan('Queue: Relationship: ' + job.name, () => processer(job)); }, { ...baseWorkerOptions(this.config, QUEUE.RELATIONSHIP), autorun: false, @@ -462,12 +417,10 @@ export class QueueProcessorService implements OnApplicationShutdown { .on('completed', (job, result) => logger.debug(`completed(${result}) id=${job.id}`)) .on('failed', (job, err) => { logger.error(`failed(${err.name}: ${err.message}) id=${job?.id ?? '?'}`, { job: renderJob(job), e: renderError(err) }); - if (Sentry != null) { - Sentry.captureMessage(`Queue: Relationship: ${job?.name ?? '?'}: ${err.name}: ${err.message}`, { - level: 'error', - extra: { job, err }, - }); - } + this.telemetryService.captureMessage(`Queue: Relationship: ${job?.name ?? '?'}: ${err.name}: ${err.message}`, { + level: 'error', + extra: { job, err }, + }); }) .on('error', (err: Error) => logger.error(`error ${err.name}: ${err.message}`, { e: renderError(err) })) .on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`)); @@ -485,11 +438,7 @@ export class QueueProcessorService implements OnApplicationShutdown { }; this.objectStorageQueueWorker = new Bull.Worker(QUEUE.OBJECT_STORAGE, (job) => { - if (Sentry != null) { - return Sentry.startSpan({ name: 'Queue: ObjectStorage: ' + job.name }, () => processer(job)); - } else { - return processer(job); - } + return this.telemetryService.startSpan('Queue: ObjectStorage: ' + job.name, () => processer(job)); }, { ...baseWorkerOptions(this.config, QUEUE.OBJECT_STORAGE), autorun: false, @@ -503,12 +452,10 @@ export class QueueProcessorService implements OnApplicationShutdown { .on('completed', (job, result) => logger.debug(`completed(${result}) id=${job.id}`)) .on('failed', (job, err) => { logger.error(`failed(${err.name}: ${err.message}) id=${job?.id ?? '?'}`, { job: renderJob(job), e: renderError(err) }); - if (Sentry != null) { - Sentry.captureMessage(`Queue: ObjectStorage: ${job?.name ?? '?'}: ${err.name}: ${err.message}`, { - level: 'error', - extra: { job, err }, - }); - } + this.telemetryService.captureMessage(`Queue: ObjectStorage: ${job?.name ?? '?'}: ${err.name}: ${err.message}`, { + level: 'error', + extra: { job, err }, + }); }) .on('error', (err: Error) => logger.error(`error ${err.name}: ${err.message}`, { e: renderError(err) })) .on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`)); @@ -518,11 +465,7 @@ export class QueueProcessorService implements OnApplicationShutdown { //#region ended poll notification { this.endedPollNotificationQueueWorker = new Bull.Worker(QUEUE.ENDED_POLL_NOTIFICATION, (job) => { - if (Sentry != null) { - return Sentry.startSpan({ name: 'Queue: EndedPollNotification' }, () => this.endedPollNotificationProcessorService.process(job)); - } else { - return this.endedPollNotificationProcessorService.process(job); - } + return this.telemetryService.startSpan('Queue: EndedPollNotification', () => this.endedPollNotificationProcessorService.process(job)); }, { ...baseWorkerOptions(this.config, QUEUE.ENDED_POLL_NOTIFICATION), autorun: false, @@ -533,11 +476,7 @@ export class QueueProcessorService implements OnApplicationShutdown { //#region post scheduled note { this.postScheduledNoteQueueWorker = new Bull.Worker(QUEUE.POST_SCHEDULED_NOTE, async (job) => { - if (Sentry != null) { - return Sentry.startSpan({ name: 'Queue: PostScheduledNote' }, () => this.postScheduledNoteProcessorService.process(job)); - } else { - return this.postScheduledNoteProcessorService.process(job); - } + return this.telemetryService.startSpan('Queue: PostScheduledNote', () => this.postScheduledNoteProcessorService.process(job)); }, { ...baseWorkerOptions(this.config, QUEUE.POST_SCHEDULED_NOTE), autorun: false, diff --git a/packages/backend/src/server/api/ApiCallService.ts b/packages/backend/src/server/api/ApiCallService.ts index 0ccb3df631..dc3a88ca27 100644 --- a/packages/backend/src/server/api/ApiCallService.ts +++ b/packages/backend/src/server/api/ApiCallService.ts @@ -16,6 +16,7 @@ import type { MiMeta, UserIpsRepository } from '@/models/_.js'; import { createTemp } from '@/misc/create-temp.js'; import { bindThis } from '@/decorators.js'; import { RoleService } from '@/core/RoleService.js'; +import { TelemetryService } from '@/core/telemetry/TelemetryService.js'; import type { Config } from '@/config.js'; import { ApiError } from './error.js'; import { RateLimiterService } from './RateLimiterService.js'; @@ -36,7 +37,6 @@ export class ApiCallService implements OnApplicationShutdown { private logger: Logger; private userIpHistories: Map>; private userIpHistoriesClearIntervalId: NodeJS.Timeout; - private Sentry: typeof import('@sentry/node') | null = null; constructor( @Inject(DI.meta) @@ -52,6 +52,7 @@ export class ApiCallService implements OnApplicationShutdown { private rateLimiterService: RateLimiterService, private roleService: RoleService, private apiLoggerService: ApiLoggerService, + private telemetryService: TelemetryService, ) { this.logger = this.apiLoggerService.logger; this.userIpHistories = new Map>(); @@ -59,12 +60,6 @@ export class ApiCallService implements OnApplicationShutdown { this.userIpHistoriesClearIntervalId = setInterval(() => { this.userIpHistories.clear(); }, 1000 * 60 * 60); - - if (this.config.sentryForBackend) { - import('@sentry/node').then((Sentry) => { - this.Sentry = Sentry; - }); - } } #sendApiError(reply: FastifyReply, err: ApiError): void { @@ -126,24 +121,20 @@ export class ApiCallService implements OnApplicationShutdown { }, }); - if (this.Sentry != null) { - this.Sentry.captureMessage(`Internal error occurred in ${ep.name}: ${err.message}`, { - level: 'error', - user: { - id: userId, + this.telemetryService.captureMessage(`Internal error occurred in ${ep.name}: ${err.message}`, { + level: 'error', + userId, + extra: { + ep: ep.name, + ps: data, + e: { + message: err.message, + code: err.name, + stack: err.stack, + id: errId, }, - extra: { - ep: ep.name, - ps: data, - e: { - message: err.message, - code: err.name, - stack: err.stack, - id: errId, - }, - }, - }); - } + }, + }); throw new ApiError(null, { e: { @@ -441,15 +432,8 @@ export class ApiCallService implements OnApplicationShutdown { } // API invoking - if (this.Sentry != null) { - return await this.Sentry.startSpan({ - name: 'API: ' + ep.name, - }, () => ep.exec(data, user, token, file, request.ip, request.headers) - .catch((err: Error) => this.#onExecError(ep, data, err, user?.id))); - } else { - return await ep.exec(data, user, token, file, request.ip, request.headers) - .catch((err: Error) => this.#onExecError(ep, data, err, user?.id)); - } + return await this.telemetryService.startSpan('API: ' + ep.name, () => ep.exec(data, user, token, file, request.ip, request.headers) + .catch((err: Error) => this.#onExecError(ep, data, err, user?.id))); } @bindThis diff --git a/packages/frontend/src/boot/common.ts b/packages/frontend/src/boot/common.ts index fa60ec4b58..4c5e601dae 100644 --- a/packages/frontend/src/boot/common.ts +++ b/packages/frontend/src/boot/common.ts @@ -5,7 +5,7 @@ import { watch, version as vueVersion } from 'vue'; import { compareVersions } from 'compare-versions'; -import { version, lang, apiUrl, isSafeMode } from '@@/js/config.js'; +import { version, lang, isSafeMode } from '@@/js/config.js'; import defaultLightTheme from '@@/themes/l-light.json5'; import defaultDarkTheme from '@@/themes/d-green-lime.json5'; import { storeBootloaderErrors } from '@@/js/store-boot-errors'; @@ -30,6 +30,7 @@ import { fetchCustomEmojis } from '@/custom-emojis.js'; import { prefer } from '@/preferences.js'; import { $i } from '@/i.js'; import { launchPlugins } from '@/plugin.js'; +import { initTelemetry } from '@/telemetry.js'; export async function common(createVue: () => Promise>) { console.info(`Misskey v${version}`); @@ -286,40 +287,7 @@ export async function common(createVue: () => Promise>) { return root; })(); - if (instance.sentryForFrontend) { - const Sentry = await import('@sentry/vue'); - Sentry.init({ - app, - integrations: [ - ...(instance.sentryForFrontend.vueIntegration !== undefined ? [ - Sentry.vueIntegration(instance.sentryForFrontend.vueIntegration ?? undefined), - ] : []), - ...(instance.sentryForFrontend.browserTracingIntegration !== undefined ? [ - Sentry.browserTracingIntegration(instance.sentryForFrontend.browserTracingIntegration ?? undefined), - ] : []), - ...(instance.sentryForFrontend.replayIntegration !== undefined ? [ - Sentry.replayIntegration(instance.sentryForFrontend.replayIntegration ?? undefined), - ] : []), - ], - - // Set tracesSampleRate to 1.0 to capture 100% - tracesSampleRate: 1.0, - - // Set `tracePropagationTargets` to control for which URLs distributed tracing should be enabled - ...(instance.sentryForFrontend.browserTracingIntegration !== undefined ? { - tracePropagationTargets: [apiUrl], - } : {}), - - // Capture Replay for 10% of all sessions, - // plus for 100% of sessions with an error - ...(instance.sentryForFrontend.replayIntegration !== undefined ? { - replaysSessionSampleRate: 0.1, - replaysOnErrorSampleRate: 1.0, - } : {}), - - ...instance.sentryForFrontend.options, - }); - } + await initTelemetry(instance, app); try { await launchPlugins(); diff --git a/packages/frontend/src/telemetry.ts b/packages/frontend/src/telemetry.ts new file mode 100644 index 0000000000..5a9d64c2a4 --- /dev/null +++ b/packages/frontend/src/telemetry.ts @@ -0,0 +1,45 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { apiUrl } from '@@/js/config.js'; +import type { App } from 'vue'; +import type * as Misskey from 'misskey-js'; + +export async function initTelemetry(instance: Misskey.entities.MetaDetailed, app: App): Promise { + if (!instance.sentryForFrontend) return; + + const Sentry = await import('@sentry/vue'); + Sentry.init({ + app, + integrations: [ + ...(instance.sentryForFrontend.vueIntegration !== undefined ? [ + Sentry.vueIntegration(instance.sentryForFrontend.vueIntegration ?? undefined), + ] : []), + ...(instance.sentryForFrontend.browserTracingIntegration !== undefined ? [ + Sentry.browserTracingIntegration(instance.sentryForFrontend.browserTracingIntegration ?? undefined), + ] : []), + ...(instance.sentryForFrontend.replayIntegration !== undefined ? [ + Sentry.replayIntegration(instance.sentryForFrontend.replayIntegration ?? undefined), + ] : []), + ], + + // Set tracesSampleRate to 1.0 to capture 100% + tracesSampleRate: 1.0, + + // Set `tracePropagationTargets` to control for which URLs distributed tracing should be enabled + ...(instance.sentryForFrontend.browserTracingIntegration !== undefined ? { + tracePropagationTargets: [apiUrl], + } : {}), + + // Capture Replay for 10% of all sessions, + // plus for 100% of sessions with an error + ...(instance.sentryForFrontend.replayIntegration !== undefined ? { + replaysSessionSampleRate: 0.1, + replaysOnErrorSampleRate: 1.0, + } : {}), + + ...instance.sentryForFrontend.options, + }); +}