mirror of
https://github.com/misskey-dev/misskey.git
synced 2026-07-27 02:54:36 +02:00
enhance: OTel設定時、JSONログにtrace_id, span_id, trace_flagがつくように (#17748)
* enhance: OTel設定時、JSONログにtrace_id, span_id, trace_flagがつくように * fix
This commit is contained in:
@@ -11,6 +11,7 @@ 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 { runQueueJobWithTraceContext } from './queue-job-runner.js';
|
||||
import { UserWebhookDeliverProcessorService } from './processors/UserWebhookDeliverProcessorService.js';
|
||||
import { SystemWebhookDeliverProcessorService } from './processors/SystemWebhookDeliverProcessorService.js';
|
||||
import { EndedPollNotificationProcessorService } from './processors/EndedPollNotificationProcessorService.js';
|
||||
@@ -176,26 +177,30 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||
default: throw new Error(`unrecognized job type ${job.name} for system`);
|
||||
}
|
||||
};
|
||||
const logger = this.logger.createSubLogger('system');
|
||||
|
||||
this.systemQueueWorker = new Bull.Worker(QUEUE.SYSTEM, (job) => {
|
||||
return this.telemetryService.startSpanWithTraceContext('Queue: System: ' + job.name, job.data, () => processer(job));
|
||||
return runQueueJobWithTraceContext(
|
||||
this.telemetryService,
|
||||
'Queue: System: ' + job.name,
|
||||
job.data,
|
||||
() => processer(job) as Promise<void>,
|
||||
err => {
|
||||
logger.error(`failed(${err.name}: ${err.message}) id=${job.id}`, { job: renderJob(job), e: renderError(err) });
|
||||
this.telemetryService.captureMessage(`Queue: System: ${job.name}: ${err.name}: ${err.message}`, {
|
||||
level: 'error',
|
||||
extra: { job, err },
|
||||
});
|
||||
},
|
||||
);
|
||||
}, {
|
||||
...baseWorkerOptions(this.config, QUEUE.SYSTEM),
|
||||
autorun: false,
|
||||
});
|
||||
|
||||
const logger = this.logger.createSubLogger('system');
|
||||
|
||||
this.systemQueueWorker
|
||||
.on('active', (job) => logger.debug(`active id=${job.id}`))
|
||||
.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) });
|
||||
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}`));
|
||||
}
|
||||
@@ -227,26 +232,30 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||
default: throw new Error(`unrecognized job type ${job.name} for db`);
|
||||
}
|
||||
};
|
||||
const logger = this.logger.createSubLogger('db');
|
||||
|
||||
this.dbQueueWorker = new Bull.Worker(QUEUE.DB, (job) => {
|
||||
return this.telemetryService.startSpanWithTraceContext('Queue: DB: ' + job.name, job.data, () => processer(job));
|
||||
return runQueueJobWithTraceContext(
|
||||
this.telemetryService,
|
||||
'Queue: DB: ' + job.name,
|
||||
job.data,
|
||||
() => processer(job),
|
||||
err => {
|
||||
logger.error(`failed(${err.name}: ${err.message}) id=${job.id}`, { job: renderJob(job), e: renderError(err) });
|
||||
this.telemetryService.captureMessage(`Queue: DB: ${job.name}: ${err.name}: ${err.message}`, {
|
||||
level: 'error',
|
||||
extra: { job, err },
|
||||
});
|
||||
},
|
||||
);
|
||||
}, {
|
||||
...baseWorkerOptions(this.config, QUEUE.DB),
|
||||
autorun: false,
|
||||
});
|
||||
|
||||
const logger = this.logger.createSubLogger('db');
|
||||
|
||||
this.dbQueueWorker
|
||||
.on('active', (job) => logger.debug(`active id=${job.id}`))
|
||||
.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) });
|
||||
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}`));
|
||||
}
|
||||
@@ -254,8 +263,22 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||
|
||||
//#region deliver
|
||||
{
|
||||
const logger = this.logger.createSubLogger('deliver');
|
||||
|
||||
this.deliverQueueWorker = new Bull.Worker(QUEUE.DELIVER, (job) => {
|
||||
return this.telemetryService.startSpanWithTraceContext('Queue: Deliver', job.data, () => this.deliverProcessorService.process(job));
|
||||
return runQueueJobWithTraceContext(
|
||||
this.telemetryService,
|
||||
'Queue: Deliver',
|
||||
job.data,
|
||||
() => this.deliverProcessorService.process(job),
|
||||
err => {
|
||||
logger.error(`failed(${err.name}: ${err.message}) ${getJobInfo(job)} to=${job.data.to}`, { e: renderError(err) });
|
||||
this.telemetryService.captureMessage(`Queue: Deliver: ${err.name}: ${err.message}`, {
|
||||
level: 'error',
|
||||
extra: { job, err },
|
||||
});
|
||||
},
|
||||
);
|
||||
}, {
|
||||
...baseWorkerOptions(this.config, QUEUE.DELIVER),
|
||||
autorun: false,
|
||||
@@ -269,18 +292,9 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||
},
|
||||
});
|
||||
|
||||
const logger = this.logger.createSubLogger('deliver');
|
||||
|
||||
this.deliverQueueWorker
|
||||
.on('active', (job) => logger.debug(`active ${getJobInfo(job, true)} to=${job.data.to}`))
|
||||
.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 : '-'}`);
|
||||
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}`));
|
||||
}
|
||||
@@ -288,8 +302,23 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||
|
||||
//#region inbox
|
||||
{
|
||||
const logger = this.logger.createSubLogger('inbox');
|
||||
|
||||
this.inboxQueueWorker = new Bull.Worker(QUEUE.INBOX, (job) => {
|
||||
return this.telemetryService.startSpanWithTraceContext('Queue: Inbox', job.data, () => this.inboxProcessorService.process(job));
|
||||
return runQueueJobWithTraceContext(
|
||||
this.telemetryService,
|
||||
'Queue: Inbox',
|
||||
job.data,
|
||||
() => this.inboxProcessorService.process(job),
|
||||
err => {
|
||||
const activityId = job.data.activity ? job.data.activity.id : 'none';
|
||||
logger.error(`failed(${err.name}: ${err.message}) ${getJobInfo(job)} activity=${activityId}`, { job: renderJob(job), e: renderError(err) });
|
||||
this.telemetryService.captureMessage(`Queue: Inbox: ${err.name}: ${err.message}`, {
|
||||
level: 'error',
|
||||
extra: { job, err },
|
||||
});
|
||||
},
|
||||
);
|
||||
}, {
|
||||
...baseWorkerOptions(this.config, QUEUE.INBOX),
|
||||
autorun: false,
|
||||
@@ -303,18 +332,9 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||
},
|
||||
});
|
||||
|
||||
const logger = this.logger.createSubLogger('inbox');
|
||||
|
||||
this.inboxQueueWorker
|
||||
.on('active', (job) => logger.debug(`active ${getJobInfo(job, true)}`))
|
||||
.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) });
|
||||
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}`));
|
||||
}
|
||||
@@ -322,8 +342,22 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||
|
||||
//#region user-webhook deliver
|
||||
{
|
||||
const logger = this.logger.createSubLogger('user-webhook');
|
||||
|
||||
this.userWebhookDeliverQueueWorker = new Bull.Worker(QUEUE.USER_WEBHOOK_DELIVER, (job) => {
|
||||
return this.telemetryService.startSpanWithTraceContext('Queue: UserWebhookDeliver', job.data, () => this.userWebhookDeliverProcessorService.process(job));
|
||||
return runQueueJobWithTraceContext(
|
||||
this.telemetryService,
|
||||
'Queue: UserWebhookDeliver',
|
||||
job.data,
|
||||
() => this.userWebhookDeliverProcessorService.process(job),
|
||||
err => {
|
||||
logger.error(`failed(${err.name}: ${err.message}) ${getJobInfo(job)} to=${job.data.to}`, { e: renderError(err) });
|
||||
this.telemetryService.captureMessage(`Queue: UserWebhookDeliver: ${err.name}: ${err.message}`, {
|
||||
level: 'error',
|
||||
extra: { job, err },
|
||||
});
|
||||
},
|
||||
);
|
||||
}, {
|
||||
...baseWorkerOptions(this.config, QUEUE.USER_WEBHOOK_DELIVER),
|
||||
autorun: false,
|
||||
@@ -337,18 +371,9 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||
},
|
||||
});
|
||||
|
||||
const logger = this.logger.createSubLogger('user-webhook');
|
||||
|
||||
this.userWebhookDeliverQueueWorker
|
||||
.on('active', (job) => logger.debug(`active ${getJobInfo(job, true)} to=${job.data.to}`))
|
||||
.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 : '-'}`);
|
||||
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}`));
|
||||
}
|
||||
@@ -356,8 +381,22 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||
|
||||
//#region system-webhook deliver
|
||||
{
|
||||
const logger = this.logger.createSubLogger('system-webhook');
|
||||
|
||||
this.systemWebhookDeliverQueueWorker = new Bull.Worker(QUEUE.SYSTEM_WEBHOOK_DELIVER, (job) => {
|
||||
return this.telemetryService.startSpanWithTraceContext('Queue: SystemWebhookDeliver', job.data, () => this.systemWebhookDeliverProcessorService.process(job));
|
||||
return runQueueJobWithTraceContext(
|
||||
this.telemetryService,
|
||||
'Queue: SystemWebhookDeliver',
|
||||
job.data,
|
||||
() => this.systemWebhookDeliverProcessorService.process(job),
|
||||
err => {
|
||||
logger.error(`failed(${err.name}: ${err.message}) ${getJobInfo(job)} to=${job.data.to}`, { e: renderError(err) });
|
||||
this.telemetryService.captureMessage(`Queue: SystemWebhookDeliver: ${err.name}: ${err.message}`, {
|
||||
level: 'error',
|
||||
extra: { job, err },
|
||||
});
|
||||
},
|
||||
);
|
||||
}, {
|
||||
...baseWorkerOptions(this.config, QUEUE.SYSTEM_WEBHOOK_DELIVER),
|
||||
autorun: false,
|
||||
@@ -371,18 +410,9 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||
},
|
||||
});
|
||||
|
||||
const logger = this.logger.createSubLogger('system-webhook');
|
||||
|
||||
this.systemWebhookDeliverQueueWorker
|
||||
.on('active', (job) => logger.debug(`active ${getJobInfo(job, true)} to=${job.data.to}`))
|
||||
.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 : '-'}`);
|
||||
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}`));
|
||||
}
|
||||
@@ -399,9 +429,22 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||
default: throw new Error(`unrecognized job type ${job.name} for relationship`);
|
||||
}
|
||||
};
|
||||
const logger = this.logger.createSubLogger('relationship');
|
||||
|
||||
this.relationshipQueueWorker = new Bull.Worker(QUEUE.RELATIONSHIP, (job) => {
|
||||
return this.telemetryService.startSpanWithTraceContext('Queue: Relationship: ' + job.name, job.data, () => processer(job));
|
||||
return runQueueJobWithTraceContext(
|
||||
this.telemetryService,
|
||||
'Queue: Relationship: ' + job.name,
|
||||
job.data,
|
||||
() => processer(job),
|
||||
err => {
|
||||
logger.error(`failed(${err.name}: ${err.message}) id=${job.id}`, { job: renderJob(job), e: renderError(err) });
|
||||
this.telemetryService.captureMessage(`Queue: Relationship: ${job.name}: ${err.name}: ${err.message}`, {
|
||||
level: 'error',
|
||||
extra: { job, err },
|
||||
});
|
||||
},
|
||||
);
|
||||
}, {
|
||||
...baseWorkerOptions(this.config, QUEUE.RELATIONSHIP),
|
||||
autorun: false,
|
||||
@@ -412,18 +455,9 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||
},
|
||||
});
|
||||
|
||||
const logger = this.logger.createSubLogger('relationship');
|
||||
|
||||
this.relationshipQueueWorker
|
||||
.on('active', (job) => logger.debug(`active id=${job.id}`))
|
||||
.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) });
|
||||
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}`));
|
||||
}
|
||||
@@ -438,27 +472,31 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||
default: throw new Error(`unrecognized job type ${job.name} for objectStorage`);
|
||||
}
|
||||
};
|
||||
const logger = this.logger.createSubLogger('objectStorage');
|
||||
|
||||
this.objectStorageQueueWorker = new Bull.Worker(QUEUE.OBJECT_STORAGE, (job) => {
|
||||
return this.telemetryService.startSpanWithTraceContext('Queue: ObjectStorage: ' + job.name, job.data, () => processer(job));
|
||||
return runQueueJobWithTraceContext(
|
||||
this.telemetryService,
|
||||
'Queue: ObjectStorage: ' + job.name,
|
||||
job.data,
|
||||
() => processer(job) as Promise<void>,
|
||||
err => {
|
||||
logger.error(`failed(${err.name}: ${err.message}) id=${job.id}`, { job: renderJob(job), e: renderError(err) });
|
||||
this.telemetryService.captureMessage(`Queue: ObjectStorage: ${job.name}: ${err.name}: ${err.message}`, {
|
||||
level: 'error',
|
||||
extra: { job, err },
|
||||
});
|
||||
},
|
||||
);
|
||||
}, {
|
||||
...baseWorkerOptions(this.config, QUEUE.OBJECT_STORAGE),
|
||||
autorun: false,
|
||||
concurrency: 16,
|
||||
});
|
||||
|
||||
const logger = this.logger.createSubLogger('objectStorage');
|
||||
|
||||
this.objectStorageQueueWorker
|
||||
.on('active', (job) => logger.debug(`active id=${job.id}`))
|
||||
.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) });
|
||||
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}`));
|
||||
}
|
||||
@@ -466,8 +504,22 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||
|
||||
//#region ended poll notification
|
||||
{
|
||||
const logger = this.logger.createSubLogger('ended-poll-notification');
|
||||
|
||||
this.endedPollNotificationQueueWorker = new Bull.Worker(QUEUE.ENDED_POLL_NOTIFICATION, (job) => {
|
||||
return this.telemetryService.startSpanWithTraceContext('Queue: EndedPollNotification', job.data, () => this.endedPollNotificationProcessorService.process(job));
|
||||
return runQueueJobWithTraceContext(
|
||||
this.telemetryService,
|
||||
'Queue: EndedPollNotification',
|
||||
job.data,
|
||||
() => this.endedPollNotificationProcessorService.process(job),
|
||||
err => {
|
||||
logger.error(`failed(${err.name}: ${err.message}) id=${job.id}`, { job: renderJob(job), e: renderError(err) });
|
||||
this.telemetryService.captureMessage(`Queue: EndedPollNotification: ${err.name}: ${err.message}`, {
|
||||
level: 'error',
|
||||
extra: { job, err },
|
||||
});
|
||||
},
|
||||
);
|
||||
}, {
|
||||
...baseWorkerOptions(this.config, QUEUE.ENDED_POLL_NOTIFICATION),
|
||||
autorun: false,
|
||||
@@ -477,8 +529,22 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||
|
||||
//#region post scheduled note
|
||||
{
|
||||
this.postScheduledNoteQueueWorker = new Bull.Worker(QUEUE.POST_SCHEDULED_NOTE, async (job) => {
|
||||
return this.telemetryService.startSpanWithTraceContext('Queue: PostScheduledNote', job.data, () => this.postScheduledNoteProcessorService.process(job));
|
||||
const logger = this.logger.createSubLogger('post-scheduled-note');
|
||||
|
||||
this.postScheduledNoteQueueWorker = new Bull.Worker(QUEUE.POST_SCHEDULED_NOTE, (job) => {
|
||||
return runQueueJobWithTraceContext(
|
||||
this.telemetryService,
|
||||
'Queue: PostScheduledNote',
|
||||
job.data,
|
||||
() => this.postScheduledNoteProcessorService.process(job),
|
||||
err => {
|
||||
logger.error(`failed(${err.name}: ${err.message}) id=${job.id}`, { job: renderJob(job), e: renderError(err) });
|
||||
this.telemetryService.captureMessage(`Queue: PostScheduledNote: ${err.name}: ${err.message}`, {
|
||||
level: 'error',
|
||||
extra: { job, err },
|
||||
});
|
||||
},
|
||||
);
|
||||
}, {
|
||||
...baseWorkerOptions(this.config, QUEUE.POST_SCHEDULED_NOTE),
|
||||
autorun: false,
|
||||
|
||||
32
packages/backend/src/queue/queue-job-runner.ts
Normal file
32
packages/backend/src/queue/queue-job-runner.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import type { TelemetryService } from '@/core/telemetry/TelemetryService.js';
|
||||
|
||||
type QueueTelemetryService = Pick<TelemetryService, 'startSpanWithTraceContext'>;
|
||||
|
||||
/** QueueのprocessorをTrace Context付きで実行し、失敗処理をSpan内で行います。 */
|
||||
export function runQueueJobWithTraceContext<T>(
|
||||
telemetryService: QueueTelemetryService,
|
||||
spanName: string,
|
||||
jobData: object,
|
||||
processJob: () => T | Promise<T>,
|
||||
onError: (error: Error) => void,
|
||||
): Promise<T> {
|
||||
return telemetryService.startSpanWithTraceContext(spanName, jobData, async (): Promise<T> => {
|
||||
try {
|
||||
return await processJob();
|
||||
} catch (error) {
|
||||
// 失敗イベントを待たず、processor Spanがactiveな間にログと通知を行います。
|
||||
const normalizedError = error instanceof Error ? error : new Error(String(error));
|
||||
try {
|
||||
onError(normalizedError);
|
||||
} catch {
|
||||
// 失敗ログの処理が例外を投げても、Queueへは元のエラーを返します。
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user