prometheus - stage 1 deployment
Some checks failed
Lint / pnpm_install (push) Successful in 1m49s
Test (backend) / e2e (22.11.0) (push) Failing after 1m47s
Test (production install and build) / production (22.11.0) (push) Successful in 1m10s
Publish Docker image / Build (push) Successful in 5m9s
Test (backend) / unit (22.11.0) (push) Failing after 5m24s
Lint / lint (backend) (push) Successful in 2m36s
Lint / lint (frontend) (push) Successful in 2m25s
Lint / lint (frontend-embed) (push) Successful in 2m24s
Lint / lint (frontend-shared) (push) Successful in 2m34s
Lint / lint (misskey-bubble-game) (push) Successful in 2m34s
Lint / lint (misskey-js) (push) Successful in 2m33s
Lint / lint (misskey-reversi) (push) Successful in 2m27s
Lint / lint (sw) (push) Successful in 2m28s
Lint / typecheck (backend) (push) Successful in 2m24s
Lint / typecheck (misskey-js) (push) Successful in 1m42s
Lint / typecheck (sw) (push) Successful in 1m43s
Some checks failed
Lint / pnpm_install (push) Successful in 1m49s
Test (backend) / e2e (22.11.0) (push) Failing after 1m47s
Test (production install and build) / production (22.11.0) (push) Successful in 1m10s
Publish Docker image / Build (push) Successful in 5m9s
Test (backend) / unit (22.11.0) (push) Failing after 5m24s
Lint / lint (backend) (push) Successful in 2m36s
Lint / lint (frontend) (push) Successful in 2m25s
Lint / lint (frontend-embed) (push) Successful in 2m24s
Lint / lint (frontend-shared) (push) Successful in 2m34s
Lint / lint (misskey-bubble-game) (push) Successful in 2m34s
Lint / lint (misskey-js) (push) Successful in 2m33s
Lint / lint (misskey-reversi) (push) Successful in 2m27s
Lint / lint (sw) (push) Successful in 2m28s
Lint / typecheck (backend) (push) Successful in 2m24s
Lint / typecheck (misskey-js) (push) Successful in 1m42s
Lint / typecheck (sw) (push) Successful in 1m43s
Signed-off-by: eternal-flame-AD <yume@yumechi.jp>
This commit is contained in:
parent
13e50cd8d9
commit
26c9e42d0b
25 changed files with 665 additions and 67 deletions
|
@ -153,6 +153,13 @@ redis:
|
||||||
|
|
||||||
id: 'aidx'
|
id: 'aidx'
|
||||||
|
|
||||||
|
# ┌──────────┐
|
||||||
|
#───┘ Metrics └──────────────────────────────────────────
|
||||||
|
|
||||||
|
#prometheusMetrics:
|
||||||
|
# enable: false
|
||||||
|
# scrapeToken: '' # Set non-empty to require a bearer token for scraping
|
||||||
|
|
||||||
# ┌────────────────┐
|
# ┌────────────────┐
|
||||||
#───┘ Error tracking └──────────────────────────────────────────
|
#───┘ Error tracking └──────────────────────────────────────────
|
||||||
|
|
||||||
|
|
|
@ -147,6 +147,13 @@ redis:
|
||||||
|
|
||||||
id: 'aidx'
|
id: 'aidx'
|
||||||
|
|
||||||
|
# ┌──────────┐
|
||||||
|
#───┘ Metrics └──────────────────────────────────────────
|
||||||
|
|
||||||
|
#prometheusMetrics:
|
||||||
|
# enable: false
|
||||||
|
# scrapeToken: '' # Set non-empty to require a bearer token for scraping
|
||||||
|
|
||||||
# ┌────────────────┐
|
# ┌────────────────┐
|
||||||
#───┘ Error tracking └──────────────────────────────────────────
|
#───┘ Error tracking └──────────────────────────────────────────
|
||||||
|
|
||||||
|
|
|
@ -229,6 +229,13 @@ redis:
|
||||||
|
|
||||||
id: 'aidx'
|
id: 'aidx'
|
||||||
|
|
||||||
|
# ┌──────────┐
|
||||||
|
#───┘ Metrics └──────────────────────────────────────────
|
||||||
|
|
||||||
|
#prometheusMetrics:
|
||||||
|
# enable: false
|
||||||
|
# scrapeToken: '' # Set non-empty to require a bearer token for scraping
|
||||||
|
|
||||||
# ┌────────────────┐
|
# ┌────────────────┐
|
||||||
#───┘ Error tracking └──────────────────────────────────────────
|
#───┘ Error tracking └──────────────────────────────────────────
|
||||||
|
|
||||||
|
|
|
@ -134,8 +134,8 @@
|
||||||
"json5": "2.2.3",
|
"json5": "2.2.3",
|
||||||
"jsonld": "8.3.2",
|
"jsonld": "8.3.2",
|
||||||
"jsrsasign": "11.1.0",
|
"jsrsasign": "11.1.0",
|
||||||
"meilisearch": "0.45.0",
|
|
||||||
"juice": "11.0.0",
|
"juice": "11.0.0",
|
||||||
|
"meilisearch": "0.45.0",
|
||||||
"mfm-js": "0.24.0",
|
"mfm-js": "0.24.0",
|
||||||
"microformats-parser": "2.0.2",
|
"microformats-parser": "2.0.2",
|
||||||
"mime-types": "2.1.35",
|
"mime-types": "2.1.35",
|
||||||
|
@ -156,6 +156,7 @@
|
||||||
"pg": "8.13.1",
|
"pg": "8.13.1",
|
||||||
"pkce-challenge": "4.1.0",
|
"pkce-challenge": "4.1.0",
|
||||||
"probe-image-size": "7.2.3",
|
"probe-image-size": "7.2.3",
|
||||||
|
"prom-client": "^15.1.3",
|
||||||
"promise-limit": "2.7.0",
|
"promise-limit": "2.7.0",
|
||||||
"pug": "3.0.3",
|
"pug": "3.0.3",
|
||||||
"punycode": "2.3.1",
|
"punycode": "2.3.1",
|
||||||
|
|
|
@ -7,9 +7,11 @@ import * as fs from 'node:fs';
|
||||||
import { fileURLToPath } from 'node:url';
|
import { fileURLToPath } from 'node:url';
|
||||||
import { dirname } from 'node:path';
|
import { dirname } from 'node:path';
|
||||||
import * as os from 'node:os';
|
import * as os from 'node:os';
|
||||||
|
import * as prom from 'prom-client';
|
||||||
import cluster from 'node:cluster';
|
import cluster from 'node:cluster';
|
||||||
import chalk from 'chalk';
|
import chalk from 'chalk';
|
||||||
import chalkTemplate from 'chalk-template';
|
import chalkTemplate from 'chalk-template';
|
||||||
|
import { collectDefaultMetrics } from 'prom-client';
|
||||||
import * as Sentry from '@sentry/node';
|
import * as Sentry from '@sentry/node';
|
||||||
import { nodeProfilingIntegration } from '@sentry/profiling-node';
|
import { nodeProfilingIntegration } from '@sentry/profiling-node';
|
||||||
import Logger from '@/logger.js';
|
import Logger from '@/logger.js';
|
||||||
|
@ -29,6 +31,18 @@ const bootLogger = logger.createSubLogger('boot', 'magenta');
|
||||||
|
|
||||||
const themeColor = chalk.hex('#86b300');
|
const themeColor = chalk.hex('#86b300');
|
||||||
|
|
||||||
|
const mBuildInfo = new prom.Gauge({
|
||||||
|
name: 'misskey_build_info',
|
||||||
|
help: 'Misskey build information',
|
||||||
|
labelNames: ['gitCommit', 'gitDescribe', 'node_version']
|
||||||
|
});
|
||||||
|
|
||||||
|
mBuildInfo.set({
|
||||||
|
gitCommit: meta.gitCommit || 'unknown',
|
||||||
|
gitDescribe: meta.gitDescribe || 'unknown',
|
||||||
|
node_version: process.version
|
||||||
|
}, 1);
|
||||||
|
|
||||||
function greet() {
|
function greet() {
|
||||||
if (!envOption.quiet) {
|
if (!envOption.quiet) {
|
||||||
//#region Misskey logo
|
//#region Misskey logo
|
||||||
|
@ -64,6 +78,13 @@ export async function masterMain() {
|
||||||
await showMachineInfo(bootLogger);
|
await showMachineInfo(bootLogger);
|
||||||
showNodejsVersion();
|
showNodejsVersion();
|
||||||
config = loadConfigBoot();
|
config = loadConfigBoot();
|
||||||
|
|
||||||
|
collectDefaultMetrics({
|
||||||
|
labels: {
|
||||||
|
cluster_type: 'master',
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
//await connectDb();
|
//await connectDb();
|
||||||
if (config.pidFile) fs.writeFileSync(config.pidFile, process.pid.toString());
|
if (config.pidFile) fs.writeFileSync(config.pidFile, process.pid.toString());
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import cluster from 'node:cluster';
|
import cluster from 'node:cluster';
|
||||||
|
import { collectDefaultMetrics } from 'prom-client';
|
||||||
import * as Sentry from '@sentry/node';
|
import * as Sentry from '@sentry/node';
|
||||||
import { nodeProfilingIntegration } from '@sentry/profiling-node';
|
import { nodeProfilingIntegration } from '@sentry/profiling-node';
|
||||||
import { envOption } from '@/env.js';
|
import { envOption } from '@/env.js';
|
||||||
|
@ -16,6 +17,12 @@ import { jobQueue, server } from './common.js';
|
||||||
export async function workerMain() {
|
export async function workerMain() {
|
||||||
const config = loadConfig();
|
const config = loadConfig();
|
||||||
|
|
||||||
|
collectDefaultMetrics({
|
||||||
|
labels: {
|
||||||
|
cluster_type: 'worker',
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
if (config.sentryForBackend) {
|
if (config.sentryForBackend) {
|
||||||
Sentry.init({
|
Sentry.init({
|
||||||
integrations: [
|
integrations: [
|
||||||
|
|
|
@ -72,6 +72,9 @@ type Source = {
|
||||||
index: string;
|
index: string;
|
||||||
scope?: 'local' | 'global' | string[];
|
scope?: 'local' | 'global' | string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
prometheusMetrics?: { enable: boolean, scrapeToken?: string };
|
||||||
|
|
||||||
sentryForBackend?: { options: Partial<Sentry.NodeOptions>; enableNodeProfiling: boolean; };
|
sentryForBackend?: { options: Partial<Sentry.NodeOptions>; enableNodeProfiling: boolean; };
|
||||||
sentryForFrontend?: { options: Partial<Sentry.NodeOptions> };
|
sentryForFrontend?: { options: Partial<Sentry.NodeOptions> };
|
||||||
|
|
||||||
|
@ -199,8 +202,12 @@ export type Config = {
|
||||||
redisForJobQueue: RedisOptions & RedisOptionsSource;
|
redisForJobQueue: RedisOptions & RedisOptionsSource;
|
||||||
redisForTimelines: RedisOptions & RedisOptionsSource;
|
redisForTimelines: RedisOptions & RedisOptionsSource;
|
||||||
redisForReactions: RedisOptions & RedisOptionsSource;
|
redisForReactions: RedisOptions & RedisOptionsSource;
|
||||||
|
|
||||||
|
prometheusMetrics : { enable: boolean, scrapeToken?: string } | undefined;
|
||||||
|
|
||||||
sentryForBackend: { options: Partial<Sentry.NodeOptions>; enableNodeProfiling: boolean; } | undefined;
|
sentryForBackend: { options: Partial<Sentry.NodeOptions>; enableNodeProfiling: boolean; } | undefined;
|
||||||
sentryForFrontend: { options: Partial<Sentry.NodeOptions> } | undefined;
|
sentryForFrontend: { options: Partial<Sentry.NodeOptions> } | undefined;
|
||||||
|
|
||||||
perChannelMaxNoteCacheCount: number;
|
perChannelMaxNoteCacheCount: number;
|
||||||
perUserNotificationsMaxCount: number;
|
perUserNotificationsMaxCount: number;
|
||||||
deactivateAntennaThreshold: number;
|
deactivateAntennaThreshold: number;
|
||||||
|
@ -295,6 +302,7 @@ export function loadConfig(): Config {
|
||||||
redisForJobQueue: config.redisForJobQueue ? convertRedisOptions(config.redisForJobQueue, host) : redis,
|
redisForJobQueue: config.redisForJobQueue ? convertRedisOptions(config.redisForJobQueue, host) : redis,
|
||||||
redisForTimelines: config.redisForTimelines ? convertRedisOptions(config.redisForTimelines, host) : redis,
|
redisForTimelines: config.redisForTimelines ? convertRedisOptions(config.redisForTimelines, host) : redis,
|
||||||
redisForReactions: config.redisForReactions ? convertRedisOptions(config.redisForReactions, host) : redis,
|
redisForReactions: config.redisForReactions ? convertRedisOptions(config.redisForReactions, host) : redis,
|
||||||
|
prometheusMetrics: config.prometheusMetrics,
|
||||||
sentryForBackend: config.sentryForBackend,
|
sentryForBackend: config.sentryForBackend,
|
||||||
sentryForFrontend: config.sentryForFrontend,
|
sentryForFrontend: config.sentryForFrontend,
|
||||||
id: config.id,
|
id: config.id,
|
||||||
|
|
|
@ -18,6 +18,7 @@ import {
|
||||||
SystemWebhookDeliverJobData,
|
SystemWebhookDeliverJobData,
|
||||||
} from '../queue/types.js';
|
} from '../queue/types.js';
|
||||||
import type { Provider } from '@nestjs/common';
|
import type { Provider } from '@nestjs/common';
|
||||||
|
import { mActiveJobs, mDelayedJobs, mJobReceivedCounter, mWaitingJobs } from '@/queue/metrics.js';
|
||||||
|
|
||||||
export type SystemQueue = Bull.Queue<Record<string, unknown>>;
|
export type SystemQueue = Bull.Queue<Record<string, unknown>>;
|
||||||
export type EndedPollNotificationQueue = Bull.Queue<EndedPollNotificationJobData>;
|
export type EndedPollNotificationQueue = Bull.Queue<EndedPollNotificationJobData>;
|
||||||
|
@ -29,57 +30,71 @@ export type ObjectStorageQueue = Bull.Queue;
|
||||||
export type UserWebhookDeliverQueue = Bull.Queue<UserWebhookDeliverJobData>;
|
export type UserWebhookDeliverQueue = Bull.Queue<UserWebhookDeliverJobData>;
|
||||||
export type SystemWebhookDeliverQueue = Bull.Queue<SystemWebhookDeliverJobData>;
|
export type SystemWebhookDeliverQueue = Bull.Queue<SystemWebhookDeliverJobData>;
|
||||||
|
|
||||||
|
function withMetrics<T>(queue: Bull.Queue<T>): Bull.Queue<T> {
|
||||||
|
setInterval(async () => {
|
||||||
|
mActiveJobs.set({ queue: queue.name }, await queue.getActiveCount());
|
||||||
|
mDelayedJobs.set({ queue: queue.name }, await queue.getDelayedCount());
|
||||||
|
mWaitingJobs.set({ queue: queue.name }, await queue.getWaitingCount());
|
||||||
|
}, 2000);
|
||||||
|
|
||||||
|
queue.on('waiting', () => {
|
||||||
|
mJobReceivedCounter.inc({ queue: queue.name });
|
||||||
|
});
|
||||||
|
|
||||||
|
return queue;
|
||||||
|
}
|
||||||
|
|
||||||
const $system: Provider = {
|
const $system: Provider = {
|
||||||
provide: 'queue:system',
|
provide: 'queue:system',
|
||||||
useFactory: (config: Config) => new Bull.Queue(QUEUE.SYSTEM, baseQueueOptions(config, QUEUE.SYSTEM)),
|
useFactory: (config: Config) => withMetrics(new Bull.Queue(QUEUE.SYSTEM, baseQueueOptions(config, QUEUE.SYSTEM))),
|
||||||
inject: [DI.config],
|
inject: [DI.config],
|
||||||
};
|
};
|
||||||
|
|
||||||
const $endedPollNotification: Provider = {
|
const $endedPollNotification: Provider = {
|
||||||
provide: 'queue:endedPollNotification',
|
provide: 'queue:endedPollNotification',
|
||||||
useFactory: (config: Config) => new Bull.Queue(QUEUE.ENDED_POLL_NOTIFICATION, baseQueueOptions(config, QUEUE.ENDED_POLL_NOTIFICATION)),
|
useFactory: (config: Config) => withMetrics(new Bull.Queue(QUEUE.ENDED_POLL_NOTIFICATION, baseQueueOptions(config, QUEUE.ENDED_POLL_NOTIFICATION))),
|
||||||
inject: [DI.config],
|
inject: [DI.config],
|
||||||
};
|
};
|
||||||
|
|
||||||
const $deliver: Provider = {
|
const $deliver: Provider = {
|
||||||
provide: 'queue:deliver',
|
provide: 'queue:deliver',
|
||||||
useFactory: (config: Config) => new Bull.Queue(QUEUE.DELIVER, baseQueueOptions(config, QUEUE.DELIVER)),
|
useFactory: (config: Config) => withMetrics(new Bull.Queue(QUEUE.DELIVER, baseQueueOptions(config, QUEUE.DELIVER))),
|
||||||
inject: [DI.config],
|
inject: [DI.config],
|
||||||
};
|
};
|
||||||
|
|
||||||
const $inbox: Provider = {
|
const $inbox: Provider = {
|
||||||
provide: 'queue:inbox',
|
provide: 'queue:inbox',
|
||||||
useFactory: (config: Config) => new Bull.Queue(QUEUE.INBOX, baseQueueOptions(config, QUEUE.INBOX)),
|
useFactory: (config: Config) => withMetrics(new Bull.Queue(QUEUE.INBOX, baseQueueOptions(config, QUEUE.INBOX))),
|
||||||
inject: [DI.config],
|
inject: [DI.config],
|
||||||
};
|
};
|
||||||
|
|
||||||
const $db: Provider = {
|
const $db: Provider = {
|
||||||
provide: 'queue:db',
|
provide: 'queue:db',
|
||||||
useFactory: (config: Config) => new Bull.Queue(QUEUE.DB, baseQueueOptions(config, QUEUE.DB)),
|
useFactory: (config: Config) => withMetrics(new Bull.Queue(QUEUE.DB, baseQueueOptions(config, QUEUE.DB))),
|
||||||
inject: [DI.config],
|
inject: [DI.config],
|
||||||
};
|
};
|
||||||
|
|
||||||
const $relationship: Provider = {
|
const $relationship: Provider = {
|
||||||
provide: 'queue:relationship',
|
provide: 'queue:relationship',
|
||||||
useFactory: (config: Config) => new Bull.Queue(QUEUE.RELATIONSHIP, baseQueueOptions(config, QUEUE.RELATIONSHIP)),
|
useFactory: (config: Config) => withMetrics(new Bull.Queue(QUEUE.RELATIONSHIP, baseQueueOptions(config, QUEUE.RELATIONSHIP))),
|
||||||
inject: [DI.config],
|
inject: [DI.config],
|
||||||
};
|
};
|
||||||
|
|
||||||
const $objectStorage: Provider = {
|
const $objectStorage: Provider = {
|
||||||
provide: 'queue:objectStorage',
|
provide: 'queue:objectStorage',
|
||||||
useFactory: (config: Config) => new Bull.Queue(QUEUE.OBJECT_STORAGE, baseQueueOptions(config, QUEUE.OBJECT_STORAGE)),
|
useFactory: (config: Config) => withMetrics(new Bull.Queue(QUEUE.OBJECT_STORAGE, baseQueueOptions(config, QUEUE.OBJECT_STORAGE))),
|
||||||
inject: [DI.config],
|
inject: [DI.config],
|
||||||
};
|
};
|
||||||
|
|
||||||
const $userWebhookDeliver: Provider = {
|
const $userWebhookDeliver: Provider = {
|
||||||
provide: 'queue:userWebhookDeliver',
|
provide: 'queue:userWebhookDeliver',
|
||||||
useFactory: (config: Config) => new Bull.Queue(QUEUE.USER_WEBHOOK_DELIVER, baseQueueOptions(config, QUEUE.USER_WEBHOOK_DELIVER)),
|
useFactory: (config: Config) => withMetrics(new Bull.Queue(QUEUE.USER_WEBHOOK_DELIVER, baseQueueOptions(config, QUEUE.USER_WEBHOOK_DELIVER))),
|
||||||
inject: [DI.config],
|
inject: [DI.config],
|
||||||
};
|
};
|
||||||
|
|
||||||
const $systemWebhookDeliver: Provider = {
|
const $systemWebhookDeliver: Provider = {
|
||||||
provide: 'queue:systemWebhookDeliver',
|
provide: 'queue:systemWebhookDeliver',
|
||||||
useFactory: (config: Config) => new Bull.Queue(QUEUE.SYSTEM_WEBHOOK_DELIVER, baseQueueOptions(config, QUEUE.SYSTEM_WEBHOOK_DELIVER)),
|
useFactory: (config: Config) => withMetrics(new Bull.Queue(QUEUE.SYSTEM_WEBHOOK_DELIVER, baseQueueOptions(config, QUEUE.SYSTEM_WEBHOOK_DELIVER))),
|
||||||
inject: [DI.config],
|
inject: [DI.config],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
|
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { In } from 'typeorm';
|
import { In } from 'typeorm';
|
||||||
|
import * as prom from 'prom-client';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
import { UserFollowingService } from '@/core/UserFollowingService.js';
|
import { UserFollowingService } from '@/core/UserFollowingService.js';
|
||||||
|
@ -43,6 +44,12 @@ import type { IAccept, IAdd, IAnnounce, IBlock, ICreate, IDelete, IFlag, IFollow
|
||||||
export class ApInboxService {
|
export class ApInboxService {
|
||||||
private logger: Logger;
|
private logger: Logger;
|
||||||
|
|
||||||
|
private mInboxReceived = new prom.Counter({
|
||||||
|
name: 'misskey_ap_inbox_received_total',
|
||||||
|
help: 'Total number of activities received by AP inbox',
|
||||||
|
labelNames: ['host', 'type'],
|
||||||
|
});
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(DI.config)
|
@Inject(DI.config)
|
||||||
private config: Config,
|
private config: Config,
|
||||||
|
@ -131,34 +138,49 @@ export class ApInboxService {
|
||||||
if (actor.isSuspended) return;
|
if (actor.isSuspended) return;
|
||||||
|
|
||||||
if (isCreate(activity)) {
|
if (isCreate(activity)) {
|
||||||
|
this.mInboxReceived.inc({ host: actor.host, type: 'create' });
|
||||||
return await this.create(actor, activity);
|
return await this.create(actor, activity);
|
||||||
} else if (isDelete(activity)) {
|
} else if (isDelete(activity)) {
|
||||||
|
this.mInboxReceived.inc({ host: actor.host, type: 'delete' });
|
||||||
return await this.delete(actor, activity);
|
return await this.delete(actor, activity);
|
||||||
} else if (isUpdate(activity)) {
|
} else if (isUpdate(activity)) {
|
||||||
|
this.mInboxReceived.inc({ host: actor.host, type: 'update' });
|
||||||
return await this.update(actor, activity);
|
return await this.update(actor, activity);
|
||||||
} else if (isFollow(activity)) {
|
} else if (isFollow(activity)) {
|
||||||
|
this.mInboxReceived.inc({ host: actor.host, type: 'follow' });
|
||||||
return await this.follow(actor, activity);
|
return await this.follow(actor, activity);
|
||||||
} else if (isAccept(activity)) {
|
} else if (isAccept(activity)) {
|
||||||
|
this.mInboxReceived.inc({ host: actor.host, type: 'accept' });
|
||||||
return await this.accept(actor, activity);
|
return await this.accept(actor, activity);
|
||||||
} else if (isReject(activity)) {
|
} else if (isReject(activity)) {
|
||||||
|
this.mInboxReceived.inc({ host: actor.host, type: 'reject' });
|
||||||
return await this.reject(actor, activity);
|
return await this.reject(actor, activity);
|
||||||
} else if (isAdd(activity)) {
|
} else if (isAdd(activity)) {
|
||||||
|
this.mInboxReceived.inc({ host: actor.host, type: 'add' });
|
||||||
return await this.add(actor, activity);
|
return await this.add(actor, activity);
|
||||||
} else if (isRemove(activity)) {
|
} else if (isRemove(activity)) {
|
||||||
|
this.mInboxReceived.inc({ host: actor.host, type: 'remove' });
|
||||||
return await this.remove(actor, activity);
|
return await this.remove(actor, activity);
|
||||||
} else if (isAnnounce(activity)) {
|
} else if (isAnnounce(activity)) {
|
||||||
|
this.mInboxReceived.inc({ host: actor.host, type: 'announce' });
|
||||||
return await this.announce(actor, activity);
|
return await this.announce(actor, activity);
|
||||||
} else if (isLike(activity)) {
|
} else if (isLike(activity)) {
|
||||||
|
this.mInboxReceived.inc({ host: actor.host, type: 'like' });
|
||||||
return await this.like(actor, activity);
|
return await this.like(actor, activity);
|
||||||
} else if (isUndo(activity)) {
|
} else if (isUndo(activity)) {
|
||||||
|
this.mInboxReceived.inc({ host: actor.host, type: 'undo' });
|
||||||
return await this.undo(actor, activity);
|
return await this.undo(actor, activity);
|
||||||
} else if (isBlock(activity)) {
|
} else if (isBlock(activity)) {
|
||||||
|
this.mInboxReceived.inc({ host: actor.host, type: 'block' });
|
||||||
return await this.block(actor, activity);
|
return await this.block(actor, activity);
|
||||||
} else if (isFlag(activity)) {
|
} else if (isFlag(activity)) {
|
||||||
|
this.mInboxReceived.inc({ host: actor.host, type: 'flag' });
|
||||||
return await this.flag(actor, activity);
|
return await this.flag(actor, activity);
|
||||||
} else if (isMove(activity)) {
|
} else if (isMove(activity)) {
|
||||||
|
this.mInboxReceived.inc({ host: actor.host, type: 'move' });
|
||||||
return await this.move(actor, activity);
|
return await this.move(actor, activity);
|
||||||
} else {
|
} else {
|
||||||
|
this.mInboxReceived.inc({ host: actor.host, type: 'unknown' });
|
||||||
return `unrecognized activity type: ${activity.type}`;
|
return `unrecognized activity type: ${activity.type}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
36
packages/backend/src/misc/log-sanitization.ts
Normal file
36
packages/backend/src/misc/log-sanitization.ts
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project and yumechi
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { aidRegExp } from "./id/aid.js";
|
||||||
|
import { aidxRegExp } from "./id/aidx.js";
|
||||||
|
|
||||||
|
export function sanitizeRequestURI(uri: string): string {
|
||||||
|
const vite = /^\/vite\/.+\.([a-z0-9]{1,4})$/;
|
||||||
|
const embed_vite = /^\/embed_vite\/.+\.([a-z0-9]{1,4})$/;
|
||||||
|
|
||||||
|
if (vite.test(uri)) {
|
||||||
|
return '[vite]';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (embed_vite.test(uri)) {
|
||||||
|
return '[embed_vite]';
|
||||||
|
}
|
||||||
|
|
||||||
|
const uuid = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/g;
|
||||||
|
const username_local = /\/@\w+(\/|$)/;
|
||||||
|
const username_remote = /\/@\w+@[a-zA-Z0-9-.]+\.[a-zA-Z]{2,4}(\/|$)/;
|
||||||
|
const token = /=[0-9a-zA-Z]{16}/g;
|
||||||
|
const aidx = new RegExp(aidxRegExp.source.replace(/^\^/, '').replace(/\$$/, ''), 'g');
|
||||||
|
const aid = new RegExp(aidRegExp.source.replace(/^\^/, '').replace(/\$$/, ''), 'g');
|
||||||
|
|
||||||
|
return uri
|
||||||
|
.replace(aidx, '[aidx]')
|
||||||
|
.replace(aid, '[aid]')
|
||||||
|
.replace(token, '=[token]')
|
||||||
|
.replace(uuid, '[uuid]')
|
||||||
|
.replace(username_local, '/[username_local]/')
|
||||||
|
.replace(username_remote, '/[username_remote]/');
|
||||||
|
}
|
||||||
|
|
|
@ -7,6 +7,8 @@
|
||||||
import pg from 'pg';
|
import pg from 'pg';
|
||||||
import { DataSource, Logger } from 'typeorm';
|
import { DataSource, Logger } from 'typeorm';
|
||||||
import * as highlight from 'cli-highlight';
|
import * as highlight from 'cli-highlight';
|
||||||
|
import * as prom from 'prom-client';
|
||||||
|
import { createHash } from 'crypto';
|
||||||
import { entities as charts } from '@/core/chart/entities.js';
|
import { entities as charts } from '@/core/chart/entities.js';
|
||||||
|
|
||||||
import { MiAbuseUserReport } from '@/models/AbuseUserReport.js';
|
import { MiAbuseUserReport } from '@/models/AbuseUserReport.js';
|
||||||
|
@ -82,6 +84,7 @@ import { MiReversiGame } from '@/models/ReversiGame.js';
|
||||||
import { Config } from '@/config.js';
|
import { Config } from '@/config.js';
|
||||||
import MisskeyLogger from '@/logger.js';
|
import MisskeyLogger from '@/logger.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
|
import { MemoryKVCache } from './misc/cache.js';
|
||||||
|
|
||||||
pg.types.setTypeParser(20, Number);
|
pg.types.setTypeParser(20, Number);
|
||||||
|
|
||||||
|
@ -89,7 +92,61 @@ export const dbLogger = new MisskeyLogger('db');
|
||||||
|
|
||||||
const sqlLogger = dbLogger.createSubLogger('sql', 'gray');
|
const sqlLogger = dbLogger.createSubLogger('sql', 'gray');
|
||||||
|
|
||||||
|
type QueryTagCache = {
|
||||||
|
join: string;
|
||||||
|
from: string;
|
||||||
|
hash: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
function dedupConsecutive<T>(arr: T[]): T[] {
|
||||||
|
return arr.filter((v, i, a) => i === 0 || a[i - 1] !== v);
|
||||||
|
}
|
||||||
|
|
||||||
|
function simplifyIdentifiers(sql: string) {
|
||||||
|
return sql.replace(/"([a-zA-Z_]+)"/g, '$1');
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractQueryTags(query: string) : QueryTagCache {
|
||||||
|
const joins = query.matchAll(/(LEFT|RIGHT|INNER|OUTER) JOIN ([a-zA-Z0-9_"`.\s]+) ON/ig);
|
||||||
|
const froms = query.matchAll(/FROM ([a-zA-Z0-9_"`.]+)/ig);
|
||||||
|
|
||||||
|
const join = Array.from(joins).map(j => `${j[1]}:${simplifyIdentifiers(j[2])}`).join('|');
|
||||||
|
const from = dedupConsecutive(Array.from(froms).map(f => simplifyIdentifiers(f[1]))).join('|');
|
||||||
|
|
||||||
|
// this is not for security just to reduce metrics size just for confirmation in local environment
|
||||||
|
// all the code is public there is nothing secret in the query itself
|
||||||
|
const hash = createHash('md5');
|
||||||
|
hash.update(query);
|
||||||
|
|
||||||
|
return {
|
||||||
|
join,
|
||||||
|
from,
|
||||||
|
hash: hash.digest('hex'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
class MyCustomLogger implements Logger {
|
class MyCustomLogger implements Logger {
|
||||||
|
private queryHashCache = new MemoryKVCache<QueryTagCache>(1000 * 60 * 5); // 5m
|
||||||
|
|
||||||
|
private mQueryCounter = new prom.Counter({
|
||||||
|
name: 'misskey_postgres_query_total',
|
||||||
|
help: 'Total queries to postgres',
|
||||||
|
labelNames: ['join', 'from', 'hash'],
|
||||||
|
});
|
||||||
|
|
||||||
|
private mQueryErrorCounter = new prom.Counter({
|
||||||
|
name: 'misskey_postgres_query_error_total',
|
||||||
|
help: 'Total errors in queries to postgres',
|
||||||
|
labelNames: ['join', 'from', 'hash'],
|
||||||
|
});
|
||||||
|
|
||||||
|
private mSlowQueryHisto = new prom.Histogram({
|
||||||
|
name: 'misskey_postgres_query_slow_duration_seconds',
|
||||||
|
help: 'Duration of slow queries to postgres',
|
||||||
|
labelNames: ['join', 'from', 'hash'],
|
||||||
|
buckets: [0.1, 0.5, 1, 2, 5, 10, 30, 300],
|
||||||
|
});
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
private highlight(sql: string) {
|
private highlight(sql: string) {
|
||||||
return highlight.highlight(sql, {
|
return highlight.highlight(sql, {
|
||||||
|
@ -97,18 +154,36 @@ class MyCustomLogger implements Logger {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getQueryTags(query: string): QueryTagCache {
|
||||||
|
const existing = this.queryHashCache.get(query);
|
||||||
|
if (existing) {
|
||||||
|
return existing;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = extractQueryTags(query);
|
||||||
|
this.queryHashCache.set(query, result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public logQuery(query: string, parameters?: any[]) {
|
public logQuery(query: string, parameters?: any[]) {
|
||||||
|
const tags =
|
||||||
|
this.mQueryCounter.inc(this.getQueryTags(query));
|
||||||
|
|
||||||
sqlLogger.info(this.highlight(query).substring(0, 100));
|
sqlLogger.info(this.highlight(query).substring(0, 100));
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public logQueryError(error: string, query: string, parameters?: any[]) {
|
public logQueryError(error: string, query: string, parameters?: any[]) {
|
||||||
|
this.mQueryErrorCounter.inc(this.getQueryTags(query));
|
||||||
|
|
||||||
sqlLogger.error(this.highlight(query));
|
sqlLogger.error(this.highlight(query));
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public logQuerySlow(time: number, query: string, parameters?: any[]) {
|
public logQuerySlow(time: number, query: string, parameters?: any[]) {
|
||||||
|
this.mSlowQueryHisto.observe(this.getQueryTags(query), time );
|
||||||
|
|
||||||
sqlLogger.warn(this.highlight(query));
|
sqlLogger.warn(this.highlight(query));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -248,7 +323,7 @@ export function createPostgresDataSource(config: Config) {
|
||||||
} : false,
|
} : false,
|
||||||
logging: log,
|
logging: log,
|
||||||
logger: log ? new MyCustomLogger() : undefined,
|
logger: log ? new MyCustomLogger() : undefined,
|
||||||
maxQueryExecutionTime: 1000,
|
maxQueryExecutionTime: 500,
|
||||||
entities: entities,
|
entities: entities,
|
||||||
migrations: ['../../migration/*.js'],
|
migrations: ['../../migration/*.js'],
|
||||||
});
|
});
|
||||||
|
|
|
@ -45,6 +45,7 @@ import { CleanProcessorService } from './processors/CleanProcessorService.js';
|
||||||
import { AggregateRetentionProcessorService } from './processors/AggregateRetentionProcessorService.js';
|
import { AggregateRetentionProcessorService } from './processors/AggregateRetentionProcessorService.js';
|
||||||
import { QueueLoggerService } from './QueueLoggerService.js';
|
import { QueueLoggerService } from './QueueLoggerService.js';
|
||||||
import { QUEUE, baseQueueOptions } from './const.js';
|
import { QUEUE, baseQueueOptions } from './const.js';
|
||||||
|
import { mStalledWorkerCounter } from './metrics.js';
|
||||||
|
|
||||||
// ref. https://github.com/misskey-dev/misskey/pull/7635#issue-971097019
|
// ref. https://github.com/misskey-dev/misskey/pull/7635#issue-971097019
|
||||||
function httpRelatedBackoff(attemptsMade: number) {
|
function httpRelatedBackoff(attemptsMade: number) {
|
||||||
|
@ -194,7 +195,10 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.on('error', (err: Error) => logger.error(`error ${err.name}: ${err.message}`, { e: renderError(err) }))
|
.on('error', (err: Error) => logger.error(`error ${err.name}: ${err.message}`, { e: renderError(err) }))
|
||||||
.on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`));
|
.on('stalled', (jobId) => {
|
||||||
|
mStalledWorkerCounter.inc({ queue: QUEUE.SYSTEM });
|
||||||
|
logger.warn(`stalled id=${jobId}`);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
|
@ -251,7 +255,10 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.on('error', (err: Error) => logger.error(`error ${err.name}: ${err.message}`, { e: renderError(err) }))
|
.on('error', (err: Error) => logger.error(`error ${err.name}: ${err.message}`, { e: renderError(err) }))
|
||||||
.on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`));
|
.on('stalled', (jobId) => {
|
||||||
|
mStalledWorkerCounter.inc({ queue: QUEUE.DB });
|
||||||
|
logger.warn(`stalled id=${jobId}`);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
|
@ -291,7 +298,10 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.on('error', (err: Error) => logger.error(`error ${err.name}: ${err.message}`, { e: renderError(err) }))
|
.on('error', (err: Error) => logger.error(`error ${err.name}: ${err.message}`, { e: renderError(err) }))
|
||||||
.on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`));
|
.on('stalled', (jobId) => {
|
||||||
|
mStalledWorkerCounter.inc({ queue: QUEUE.DELIVER });
|
||||||
|
logger.warn(`stalled id=${jobId}`);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
|
@ -331,7 +341,10 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.on('error', (err: Error) => logger.error(`error ${err.name}: ${err.message}`, { e: renderError(err) }))
|
.on('error', (err: Error) => logger.error(`error ${err.name}: ${err.message}`, { e: renderError(err) }))
|
||||||
.on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`));
|
.on('stalled', (jobId) => {
|
||||||
|
mStalledWorkerCounter.inc({ queue: QUEUE.INBOX });
|
||||||
|
logger.warn(`stalled id=${jobId}`);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
|
@ -371,7 +384,10 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.on('error', (err: Error) => logger.error(`error ${err.name}: ${err.message}`, { e: renderError(err) }))
|
.on('error', (err: Error) => logger.error(`error ${err.name}: ${err.message}`, { e: renderError(err) }))
|
||||||
.on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`));
|
.on('stalled', (jobId) => {
|
||||||
|
mStalledWorkerCounter.inc({ queue: QUEUE.USER_WEBHOOK_DELIVER });
|
||||||
|
logger.warn(`stalled id=${jobId}`);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
|
@ -411,7 +427,10 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.on('error', (err: Error) => logger.error(`error ${err.name}: ${err.message}`, { e: renderError(err) }))
|
.on('error', (err: Error) => logger.error(`error ${err.name}: ${err.message}`, { e: renderError(err) }))
|
||||||
.on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`));
|
.on('stalled', (jobId) => {
|
||||||
|
mStalledWorkerCounter.inc({ queue: QUEUE.SYSTEM_WEBHOOK_DELIVER });
|
||||||
|
logger.warn(`stalled id=${jobId}`);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
|
@ -458,7 +477,10 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.on('error', (err: Error) => logger.error(`error ${err.name}: ${err.message}`, { e: renderError(err) }))
|
.on('error', (err: Error) => logger.error(`error ${err.name}: ${err.message}`, { e: renderError(err) }))
|
||||||
.on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`));
|
.on('stalled', (jobId) => {
|
||||||
|
mStalledWorkerCounter.inc({ queue: QUEUE.RELATIONSHIP });
|
||||||
|
logger.warn(`stalled id=${jobId}`);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
|
@ -499,7 +521,10 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.on('error', (err: Error) => logger.error(`error ${err.name}: ${err.message}`, { e: renderError(err) }))
|
.on('error', (err: Error) => logger.error(`error ${err.name}: ${err.message}`, { e: renderError(err) }))
|
||||||
.on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`));
|
.on('stalled', (jobId) => {
|
||||||
|
mStalledWorkerCounter.inc({ queue: QUEUE.OBJECT_STORAGE });
|
||||||
|
logger.warn(`stalled id=${jobId}`);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
|
|
31
packages/backend/src/queue/metrics.ts
Normal file
31
packages/backend/src/queue/metrics.ts
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
import * as prom from 'prom-client';
|
||||||
|
|
||||||
|
export const mJobReceivedCounter = new prom.Counter({
|
||||||
|
name: 'misskey_queue_jobs_received_total',
|
||||||
|
help: 'Total number of jobs received by queue',
|
||||||
|
labelNames: ['queue'],
|
||||||
|
});
|
||||||
|
|
||||||
|
export const mActiveJobs = new prom.Gauge({
|
||||||
|
name: 'misskey_queue_active_jobs',
|
||||||
|
help: 'Number of active jobs in queue',
|
||||||
|
labelNames: ['queue'],
|
||||||
|
});
|
||||||
|
|
||||||
|
export const mDelayedJobs = new prom.Gauge({
|
||||||
|
name: 'misskey_queue_delayed_jobs',
|
||||||
|
help: 'Number of delayed jobs in queue',
|
||||||
|
labelNames: ['queue'],
|
||||||
|
});
|
||||||
|
|
||||||
|
export const mWaitingJobs = new prom.Gauge({
|
||||||
|
name: 'misskey_queue_waiting_jobs',
|
||||||
|
help: 'Number of waiting jobs in queue',
|
||||||
|
labelNames: ['queue'],
|
||||||
|
});
|
||||||
|
|
||||||
|
export const mStalledWorkerCounter = new prom.Counter({
|
||||||
|
name: 'misskey_queue_stalled_workers_total',
|
||||||
|
help: 'Total number of stalled workers',
|
||||||
|
labelNames: ['queue'],
|
||||||
|
});
|
|
@ -7,6 +7,7 @@ import { URL } from 'node:url';
|
||||||
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
|
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
|
||||||
import httpSignature from '@peertube/http-signature';
|
import httpSignature from '@peertube/http-signature';
|
||||||
import * as Bull from 'bullmq';
|
import * as Bull from 'bullmq';
|
||||||
|
import * as prom from 'prom-client';
|
||||||
import type Logger from '@/logger.js';
|
import type Logger from '@/logger.js';
|
||||||
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
|
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
|
||||||
import { FetchInstanceMetadataService } from '@/core/FetchInstanceMetadataService.js';
|
import { FetchInstanceMetadataService } from '@/core/FetchInstanceMetadataService.js';
|
||||||
|
@ -42,6 +43,37 @@ export class InboxProcessorService implements OnApplicationShutdown {
|
||||||
private logger: Logger;
|
private logger: Logger;
|
||||||
private updateInstanceQueue: CollapsedQueue<MiNote['id'], UpdateInstanceJob>;
|
private updateInstanceQueue: CollapsedQueue<MiNote['id'], UpdateInstanceJob>;
|
||||||
|
|
||||||
|
private mIncomingApProcessingTime = new prom.Histogram({
|
||||||
|
name: 'misskey_incoming_ap_processing_time',
|
||||||
|
help: 'Incoming AP processing time in seconds',
|
||||||
|
labelNames: ['incoming_host', 'incoming_type', 'success'],
|
||||||
|
buckets: [0.01, 0.1, 0.5, 1, 5, 10, 30, 60, 300, 1800],
|
||||||
|
});
|
||||||
|
|
||||||
|
private mIncomingApEvent = new prom.Counter({
|
||||||
|
name: 'misskey_incoming_ap_event',
|
||||||
|
help: 'Incoming AP event',
|
||||||
|
labelNames: ['incoming_host', 'incoming_type'],
|
||||||
|
});
|
||||||
|
|
||||||
|
private mIncomingApEventAccepted = new prom.Counter({
|
||||||
|
name: 'misskey_incoming_ap_event_accepted',
|
||||||
|
help: 'Incoming AP event accepted',
|
||||||
|
labelNames: ['incoming_host', 'incoming_type'],
|
||||||
|
});
|
||||||
|
|
||||||
|
private mIncomingApReject = new prom.Counter({
|
||||||
|
name: 'misskey_incoming_ap_reject',
|
||||||
|
help: 'Incoming AP reject',
|
||||||
|
labelNames: ['incoming_host', 'incoming_type', 'reason'],
|
||||||
|
});
|
||||||
|
|
||||||
|
private mincomingApProcessingError = new prom.Counter({
|
||||||
|
name: 'misskey_incoming_ap_processing_error',
|
||||||
|
help: 'Incoming AP processing error',
|
||||||
|
labelNames: ['incoming_host', 'incoming_type'],
|
||||||
|
});
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(DI.meta)
|
@Inject(DI.meta)
|
||||||
private meta: MiMeta,
|
private meta: MiMeta,
|
||||||
|
@ -66,7 +98,6 @@ export class InboxProcessorService implements OnApplicationShutdown {
|
||||||
public async process(job: Bull.Job<InboxJobData>): Promise<string> {
|
public async process(job: Bull.Job<InboxJobData>): Promise<string> {
|
||||||
const signature = job.data.signature; // HTTP-signature
|
const signature = job.data.signature; // HTTP-signature
|
||||||
let activity = job.data.activity;
|
let activity = job.data.activity;
|
||||||
|
|
||||||
//#region Log
|
//#region Log
|
||||||
const info = Object.assign({}, activity);
|
const info = Object.assign({}, activity);
|
||||||
delete info['@context'];
|
delete info['@context'];
|
||||||
|
@ -75,12 +106,34 @@ export class InboxProcessorService implements OnApplicationShutdown {
|
||||||
|
|
||||||
const host = this.utilityService.toPuny(new URL(signature.keyId).hostname);
|
const host = this.utilityService.toPuny(new URL(signature.keyId).hostname);
|
||||||
|
|
||||||
|
const incCounter = <T extends 'incoming_host' | 'incoming_type', U>(counter: prom.Counter<T>, addn_labels: U) => {
|
||||||
|
if (Array.isArray(activity.type)) {
|
||||||
|
for (const t of activity.type) {
|
||||||
|
counter.inc({ incoming_host: host.toString(), incoming_type: t, ...addn_labels });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
counter.inc({ incoming_host: host.toString(), incoming_type: activity.type ?? 'unknown', ...addn_labels });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const observeHistogram = <T extends 'incoming_host' | 'incoming_type', U>(histogram: prom.Histogram<T>, addn_labels: U, value: number) => {
|
||||||
|
if (Array.isArray(activity.type)) {
|
||||||
|
for (const t of activity.type) {
|
||||||
|
histogram.observe({ incoming_host: host.toString(), incoming_type: t, ...addn_labels }, value);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
histogram.observe({ incoming_host: host.toString(), incoming_type: activity.type ?? 'unknown', ...addn_labels }, value);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (!this.utilityService.isFederationAllowedHost(host)) {
|
if (!this.utilityService.isFederationAllowedHost(host)) {
|
||||||
|
incCounter(this.mIncomingApReject, { reason: 'host_not_allowed' });
|
||||||
return `Blocked request: ${host}`;
|
return `Blocked request: ${host}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const keyIdLower = signature.keyId.toLowerCase();
|
const keyIdLower = signature.keyId.toLowerCase();
|
||||||
if (keyIdLower.startsWith('acct:')) {
|
if (keyIdLower.startsWith('acct:')) {
|
||||||
|
incCounter(this.mIncomingApReject, { reason: 'keyid_acct' });
|
||||||
return `Old keyId is no longer supported. ${keyIdLower}`;
|
return `Old keyId is no longer supported. ${keyIdLower}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -98,6 +151,7 @@ export class InboxProcessorService implements OnApplicationShutdown {
|
||||||
// 対象が4xxならスキップ
|
// 対象が4xxならスキップ
|
||||||
if (err instanceof StatusError) {
|
if (err instanceof StatusError) {
|
||||||
if (!err.isRetryable) {
|
if (!err.isRetryable) {
|
||||||
|
incCounter(this.mIncomingApReject, { reason: 'actor_key_unresolvable' });
|
||||||
throw new Bull.UnrecoverableError(`skip: Ignored deleted actors on both ends ${activity.actor} - ${err.statusCode}`);
|
throw new Bull.UnrecoverableError(`skip: Ignored deleted actors on both ends ${activity.actor} - ${err.statusCode}`);
|
||||||
}
|
}
|
||||||
throw new Error(`Error in actor ${activity.actor} - ${err.statusCode}`);
|
throw new Error(`Error in actor ${activity.actor} - ${err.statusCode}`);
|
||||||
|
@ -107,11 +161,13 @@ export class InboxProcessorService implements OnApplicationShutdown {
|
||||||
|
|
||||||
// それでもわからなければ終了
|
// それでもわからなければ終了
|
||||||
if (authUser == null) {
|
if (authUser == null) {
|
||||||
|
incCounter(this.mIncomingApReject, { reason: 'actor_unresolvable' });
|
||||||
throw new Bull.UnrecoverableError('skip: failed to resolve user');
|
throw new Bull.UnrecoverableError('skip: failed to resolve user');
|
||||||
}
|
}
|
||||||
|
|
||||||
// publicKey がなくても終了
|
// publicKey がなくても終了
|
||||||
if (authUser.key == null) {
|
if (authUser.key == null) {
|
||||||
|
incCounter(this.mIncomingApReject, { reason: 'publickey_unresolvable' });
|
||||||
throw new Bull.UnrecoverableError('skip: failed to resolve user publicKey');
|
throw new Bull.UnrecoverableError('skip: failed to resolve user publicKey');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -124,6 +180,7 @@ export class InboxProcessorService implements OnApplicationShutdown {
|
||||||
const ldSignature = activity.signature;
|
const ldSignature = activity.signature;
|
||||||
if (ldSignature) {
|
if (ldSignature) {
|
||||||
if (ldSignature.type !== 'RsaSignature2017') {
|
if (ldSignature.type !== 'RsaSignature2017') {
|
||||||
|
incCounter(this.mIncomingApReject, { reason: 'ld_signature_unsupported' });
|
||||||
throw new Bull.UnrecoverableError(`skip: unsupported LD-signature type ${ldSignature.type}`);
|
throw new Bull.UnrecoverableError(`skip: unsupported LD-signature type ${ldSignature.type}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -137,10 +194,12 @@ export class InboxProcessorService implements OnApplicationShutdown {
|
||||||
// keyIdからLD-Signatureのユーザーを取得
|
// keyIdからLD-Signatureのユーザーを取得
|
||||||
authUser = await this.apDbResolverService.getAuthUserFromKeyId(ldSignature.creator);
|
authUser = await this.apDbResolverService.getAuthUserFromKeyId(ldSignature.creator);
|
||||||
if (authUser == null) {
|
if (authUser == null) {
|
||||||
|
incCounter(this.mIncomingApReject, { reason: 'ld_signature_user_unresolvable' });
|
||||||
throw new Bull.UnrecoverableError('skip: LD-Signatureのユーザーが取得できませんでした');
|
throw new Bull.UnrecoverableError('skip: LD-Signatureのユーザーが取得できませんでした');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (authUser.key == null) {
|
if (authUser.key == null) {
|
||||||
|
incCounter(this.mIncomingApReject, { reason: 'ld_signature_publickey_unavailable' });
|
||||||
throw new Bull.UnrecoverableError('skip: LD-SignatureのユーザーはpublicKeyを持っていませんでした');
|
throw new Bull.UnrecoverableError('skip: LD-SignatureのユーザーはpublicKeyを持っていませんでした');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -149,6 +208,7 @@ export class InboxProcessorService implements OnApplicationShutdown {
|
||||||
// LD-Signature検証
|
// LD-Signature検証
|
||||||
const verified = await jsonLd.verifyRsaSignature2017(activity, authUser.key.keyPem).catch(() => false);
|
const verified = await jsonLd.verifyRsaSignature2017(activity, authUser.key.keyPem).catch(() => false);
|
||||||
if (!verified) {
|
if (!verified) {
|
||||||
|
incCounter(this.mIncomingApReject, { reason: 'ld_signature_verification_failed' });
|
||||||
throw new Bull.UnrecoverableError('skip: LD-Signatureの検証に失敗しました');
|
throw new Bull.UnrecoverableError('skip: LD-Signatureの検証に失敗しました');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -171,14 +231,17 @@ export class InboxProcessorService implements OnApplicationShutdown {
|
||||||
|
|
||||||
// もう一度actorチェック
|
// もう一度actorチェック
|
||||||
if (authUser.user.uri !== activity.actor) {
|
if (authUser.user.uri !== activity.actor) {
|
||||||
|
incCounter(this.mIncomingApReject, { reason: 'ld_signature_actor_mismatch' });
|
||||||
throw new Bull.UnrecoverableError(`skip: LD-Signature user(${authUser.user.uri}) !== activity.actor(${activity.actor})`);
|
throw new Bull.UnrecoverableError(`skip: LD-Signature user(${authUser.user.uri}) !== activity.actor(${activity.actor})`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const ldHost = this.utilityService.extractDbHost(authUser.user.uri);
|
const ldHost = this.utilityService.extractDbHost(authUser.user.uri);
|
||||||
if (!this.utilityService.isFederationAllowedHost(ldHost)) {
|
if (!this.utilityService.isFederationAllowedHost(ldHost)) {
|
||||||
|
incCounter(this.mIncomingApReject, { reason: 'fed_host_not_allowed' });
|
||||||
throw new Bull.UnrecoverableError(`Blocked request: ${ldHost}`);
|
throw new Bull.UnrecoverableError(`Blocked request: ${ldHost}`);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
incCounter(this.mIncomingApReject, { reason: 'ld_signature_unavailable' });
|
||||||
throw new Bull.UnrecoverableError(`skip: http-signature verification failed and no LD-Signature. keyId=${signature.keyId}`);
|
throw new Bull.UnrecoverableError(`skip: http-signature verification failed and no LD-Signature. keyId=${signature.keyId}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -188,6 +251,7 @@ export class InboxProcessorService implements OnApplicationShutdown {
|
||||||
const signerHost = this.utilityService.extractDbHost(authUser.user.uri!);
|
const signerHost = this.utilityService.extractDbHost(authUser.user.uri!);
|
||||||
const activityIdHost = this.utilityService.extractDbHost(activity.id);
|
const activityIdHost = this.utilityService.extractDbHost(activity.id);
|
||||||
if (signerHost !== activityIdHost) {
|
if (signerHost !== activityIdHost) {
|
||||||
|
incCounter(this.mIncomingApReject, 'host_signature_mismatch');
|
||||||
throw new Bull.UnrecoverableError(`skip: signerHost(${signerHost}) !== activity.id host(${activityIdHost}`);
|
throw new Bull.UnrecoverableError(`skip: signerHost(${signerHost}) !== activity.id host(${activityIdHost}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -215,7 +279,10 @@ export class InboxProcessorService implements OnApplicationShutdown {
|
||||||
this.fetchInstanceMetadataService.fetchInstanceMetadata(i);
|
this.fetchInstanceMetadataService.fetchInstanceMetadata(i);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
incCounter(this.mIncomingApEvent, {});
|
||||||
|
|
||||||
// アクティビティを処理
|
// アクティビティを処理
|
||||||
|
const begin = +new Date();
|
||||||
try {
|
try {
|
||||||
const result = await this.apInboxService.performActivity(authUser.user, activity);
|
const result = await this.apInboxService.performActivity(authUser.user, activity);
|
||||||
if (result && !result.startsWith('ok')) {
|
if (result && !result.startsWith('ok')) {
|
||||||
|
@ -225,17 +292,26 @@ export class InboxProcessorService implements OnApplicationShutdown {
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof IdentifiableError) {
|
if (e instanceof IdentifiableError) {
|
||||||
if (e.id === '689ee33f-f97c-479a-ac49-1b9f8140af99') {
|
if (e.id === '689ee33f-f97c-479a-ac49-1b9f8140af99') {
|
||||||
|
incCounter(this.mIncomingApReject, { reason: 'blocked_notes_with_prohibited_words' });
|
||||||
return 'blocked notes with prohibited words';
|
return 'blocked notes with prohibited words';
|
||||||
}
|
}
|
||||||
if (e.id === '85ab9bd7-3a41-4530-959d-f07073900109') {
|
if (e.id === '85ab9bd7-3a41-4530-959d-f07073900109') {
|
||||||
|
incCounter(this.mIncomingApReject, { reason: 'actor_suspended' });
|
||||||
return 'actor has been suspended';
|
return 'actor has been suspended';
|
||||||
}
|
}
|
||||||
if (e.id === 'd450b8a9-48e4-4dab-ae36-f4db763fda7c') { // invalid Note
|
if (e.id === 'd450b8a9-48e4-4dab-ae36-f4db763fda7c') { // invalid Note
|
||||||
|
incCounter(this.mIncomingApReject, { reason: 'invalid_note' });
|
||||||
return e.message;
|
return e.message;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const end = +new Date();
|
||||||
|
observeHistogram(this.mIncomingApProcessingTime, { success: 'false' }, (end - begin) / 1000);
|
||||||
|
incCounter(this.mincomingApProcessingError, { reason: 'unknown' });
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
|
observeHistogram(this.mIncomingApProcessingTime, { success: 'true' }, (+new Date() - begin) / 1000);
|
||||||
|
incCounter(this.mIncomingApEventAccepted, {});
|
||||||
|
|
||||||
return 'ok';
|
return 'ok';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -47,6 +47,7 @@ import { RoleTimelineChannelService } from './api/stream/channels/role-timeline.
|
||||||
import { ReversiChannelService } from './api/stream/channels/reversi.js';
|
import { ReversiChannelService } from './api/stream/channels/reversi.js';
|
||||||
import { ReversiGameChannelService } from './api/stream/channels/reversi-game.js';
|
import { ReversiGameChannelService } from './api/stream/channels/reversi-game.js';
|
||||||
import { SigninWithPasskeyApiService } from './api/SigninWithPasskeyApiService.js';
|
import { SigninWithPasskeyApiService } from './api/SigninWithPasskeyApiService.js';
|
||||||
|
import { MetricsService } from './api/MetricsService.js';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
|
@ -94,6 +95,7 @@ import { SigninWithPasskeyApiService } from './api/SigninWithPasskeyApiService.j
|
||||||
UserListChannelService,
|
UserListChannelService,
|
||||||
OpenApiServerService,
|
OpenApiServerService,
|
||||||
OAuth2ProviderService,
|
OAuth2ProviderService,
|
||||||
|
MetricsService,
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
ServerService,
|
ServerService,
|
||||||
|
|
|
@ -33,14 +33,88 @@ import { OpenApiServerService } from './api/openapi/OpenApiServerService.js';
|
||||||
import { OAuth2ProviderService } from './oauth/OAuth2ProviderService.js';
|
import { OAuth2ProviderService } from './oauth/OAuth2ProviderService.js';
|
||||||
import { makeHstsHook } from './hsts.js';
|
import { makeHstsHook } from './hsts.js';
|
||||||
import { generateCSP } from './csp.js';
|
import { generateCSP } from './csp.js';
|
||||||
|
import * as prom from 'prom-client';
|
||||||
|
import { sanitizeRequestURI } from '@/misc/log-sanitization.js';
|
||||||
|
import { MetricsService } from './api/MetricsService.js';
|
||||||
|
|
||||||
const _dirname = fileURLToPath(new URL('.', import.meta.url));
|
const _dirname = fileURLToPath(new URL('.', import.meta.url));
|
||||||
|
|
||||||
|
function categorizeRequestPath(path: string): 'api' | 'health' | 'vite' | 'other' {
|
||||||
|
if (path === '/healthz') {
|
||||||
|
return 'health';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (path.startsWith('/vite/') || path.startsWith('/embed_vite/')) {
|
||||||
|
return 'vite';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (path === '/api' || path.startsWith('/api/')) {
|
||||||
|
return 'api';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'other';
|
||||||
|
}
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ServerService implements OnApplicationShutdown {
|
export class ServerService implements OnApplicationShutdown {
|
||||||
private logger: Logger;
|
private logger: Logger;
|
||||||
#fastify: FastifyInstance;
|
#fastify: FastifyInstance;
|
||||||
|
|
||||||
|
private mRequestTime = new prom.Histogram({
|
||||||
|
name: 'misskey_http_request_duration_seconds',
|
||||||
|
help: 'Duration of handling HTTP requests in seconds',
|
||||||
|
labelNames: ['host', 'cate', 'method', 'path'],
|
||||||
|
buckets: [0.001, 0.1, 0.5, 1, 2, 5],
|
||||||
|
});
|
||||||
|
|
||||||
|
private mRequestsReceived = new prom.Counter({
|
||||||
|
name: 'misskey_http_requests_received_total',
|
||||||
|
help: 'Total number of HTTP requests received',
|
||||||
|
labelNames: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
private mNotFoundServed = new prom.Counter({
|
||||||
|
name: 'misskey_http_not_found_served_total',
|
||||||
|
help: 'Total number of HTTP 404 responses served',
|
||||||
|
labelNames: ['method', 'cate'],
|
||||||
|
});
|
||||||
|
|
||||||
|
private mMethodNotAllowedServed = new prom.Counter({
|
||||||
|
name: 'misskey_http_method_not_allowed_served_total',
|
||||||
|
help: 'Total number of HTTP 405 responses served',
|
||||||
|
labelNames: ['method', 'cate'],
|
||||||
|
});
|
||||||
|
|
||||||
|
private mTooManyRequestsServed = new prom.Counter({
|
||||||
|
name: 'misskey_http_too_many_requests_served_total',
|
||||||
|
help: 'Total number of HTTP 429 responses served',
|
||||||
|
labelNames: ['method', 'cate'],
|
||||||
|
});
|
||||||
|
|
||||||
|
private mAggregateRequestsServed = new prom.Counter({
|
||||||
|
name: 'misskey_http_requests_served_total',
|
||||||
|
help: 'Total number of HTTP requests served including invalid requests',
|
||||||
|
labelNames: ['host', 'cate', 'status'],
|
||||||
|
});
|
||||||
|
|
||||||
|
private mRequestsServedByPath = new prom.Counter({
|
||||||
|
name: 'misskey_http_requests_served_by_path',
|
||||||
|
help: 'Total number of HTTP requests served',
|
||||||
|
labelNames: ['host', 'cate', 'method', 'path', 'status'],
|
||||||
|
});
|
||||||
|
|
||||||
|
private mErrorCount = new prom.Counter({
|
||||||
|
name: 'misskey_http_errors_total',
|
||||||
|
help: 'Total number of HTTP errors on a valid request path',
|
||||||
|
labelNames: ['host', 'cate', 'method', 'path'],
|
||||||
|
});
|
||||||
|
|
||||||
|
private mLastSuccessfulRequest = new prom.Gauge({
|
||||||
|
name: 'misskey_http_last_successful_request_timestamp_seconds',
|
||||||
|
help: 'Unix Timestamp of the last successful HTTP request',
|
||||||
|
labelNames: [],
|
||||||
|
});
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(DI.config)
|
@Inject(DI.config)
|
||||||
private config: Config,
|
private config: Config,
|
||||||
|
@ -70,6 +144,7 @@ export class ServerService implements OnApplicationShutdown {
|
||||||
private globalEventService: GlobalEventService,
|
private globalEventService: GlobalEventService,
|
||||||
private loggerService: LoggerService,
|
private loggerService: LoggerService,
|
||||||
private oauth2ProviderService: OAuth2ProviderService,
|
private oauth2ProviderService: OAuth2ProviderService,
|
||||||
|
private metricsService: MetricsService,
|
||||||
) {
|
) {
|
||||||
this.logger = this.loggerService.getLogger('server', 'gray');
|
this.logger = this.loggerService.getLogger('server', 'gray');
|
||||||
}
|
}
|
||||||
|
@ -82,6 +157,111 @@ export class ServerService implements OnApplicationShutdown {
|
||||||
});
|
});
|
||||||
this.#fastify = fastify;
|
this.#fastify = fastify;
|
||||||
|
|
||||||
|
if (this.config.prometheusMetrics?.enable) {
|
||||||
|
fastify.addHook('onRequest', (_request, reply, done) => {
|
||||||
|
reply.header('x-request-received', (+new Date()).toString());
|
||||||
|
this.mRequestsReceived.inc();
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
fastify.addHook('onError', (request, _reply, error, done) => {
|
||||||
|
const url = new URL(request.url, this.config.url);
|
||||||
|
const logPath = sanitizeRequestURI(url.pathname);
|
||||||
|
this.mErrorCount.inc({
|
||||||
|
host: request.hostname,
|
||||||
|
method: request.method,
|
||||||
|
path: logPath,
|
||||||
|
cate: categorizeRequestPath(logPath),
|
||||||
|
});
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
fastify.addHook('onResponse', (request, reply, done) => {
|
||||||
|
const url = new URL(request.url, this.config.url);
|
||||||
|
const logPath = sanitizeRequestURI(url.pathname);
|
||||||
|
const cate = categorizeRequestPath(logPath);
|
||||||
|
const received = reply.getHeader('x-request-received') as string;
|
||||||
|
|
||||||
|
this.mAggregateRequestsServed.inc({
|
||||||
|
host: request.hostname,
|
||||||
|
cate,
|
||||||
|
status: reply.statusCode,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (reply.statusCode === 429) {
|
||||||
|
this.mTooManyRequestsServed.inc({
|
||||||
|
method: request.method,
|
||||||
|
cate,
|
||||||
|
});
|
||||||
|
|
||||||
|
done();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (reply.statusCode === 404) {
|
||||||
|
this.mNotFoundServed.inc({
|
||||||
|
method: request.method,
|
||||||
|
cate,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (received) {
|
||||||
|
const duration = (+new Date()) - parseInt(received);
|
||||||
|
this.mRequestTime.observe({
|
||||||
|
host: request.hostname,
|
||||||
|
method: request.method,
|
||||||
|
cate,
|
||||||
|
}, duration / 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
done();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (reply.statusCode === 405) {
|
||||||
|
this.mMethodNotAllowedServed.inc({
|
||||||
|
method: request.method,
|
||||||
|
cate,
|
||||||
|
});
|
||||||
|
done();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (received) {
|
||||||
|
const duration = (+new Date()) - parseInt(received);
|
||||||
|
|
||||||
|
this.mRequestTime.observe({
|
||||||
|
host: request.hostname,
|
||||||
|
method: request.method,
|
||||||
|
cate,
|
||||||
|
path: logPath,
|
||||||
|
}, duration / 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (logPath === '/metrics' || logPath === '/healthz') {
|
||||||
|
done();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (reply.statusCode <= 299) {
|
||||||
|
this.mLastSuccessfulRequest.set(+new Date() / 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.mRequestsServedByPath.inc({
|
||||||
|
host: request.hostname,
|
||||||
|
method: request.method,
|
||||||
|
path: logPath,
|
||||||
|
cate,
|
||||||
|
status: reply.statusCode >= 500 ? '5xx' :
|
||||||
|
reply.statusCode === 401 ? '401' :
|
||||||
|
reply.statusCode === 403 ? '403' :
|
||||||
|
reply.statusCode >= 400 ?
|
||||||
|
'4xx' : '2xx',
|
||||||
|
});
|
||||||
|
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// HSTS
|
// HSTS
|
||||||
if (this.config.url.startsWith('https') && !this.config.disableHsts) {
|
if (this.config.url.startsWith('https') && !this.config.disableHsts) {
|
||||||
const preload = this.config.hstsPreload;
|
const preload = this.config.hstsPreload;
|
||||||
|
@ -253,6 +433,8 @@ export class ServerService implements OnApplicationShutdown {
|
||||||
|
|
||||||
fastify.register(this.clientServerService.createServer);
|
fastify.register(this.clientServerService.createServer);
|
||||||
|
|
||||||
|
fastify.register(this.metricsService.createServer);
|
||||||
|
|
||||||
this.streamingApiServerService.attach(fastify.server);
|
this.streamingApiServerService.attach(fastify.server);
|
||||||
|
|
||||||
fastify.server.on('error', err => {
|
fastify.server.on('error', err => {
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
|
|
||||||
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
|
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
|
import * as prom from 'prom-client';
|
||||||
import type { AccessTokensRepository, AppsRepository, UsersRepository } from '@/models/_.js';
|
import type { AccessTokensRepository, AppsRepository, UsersRepository } from '@/models/_.js';
|
||||||
import type { MiLocalUser } from '@/models/User.js';
|
import type { MiLocalUser } from '@/models/User.js';
|
||||||
import type { MiAccessToken } from '@/models/AccessToken.js';
|
import type { MiAccessToken } from '@/models/AccessToken.js';
|
||||||
|
@ -25,6 +26,12 @@ export class AuthenticationError extends Error {
|
||||||
export class AuthenticateService implements OnApplicationShutdown {
|
export class AuthenticateService implements OnApplicationShutdown {
|
||||||
private appCache: MemoryKVCache<MiApp>;
|
private appCache: MemoryKVCache<MiApp>;
|
||||||
|
|
||||||
|
private mAuthenticationFailureCounter = new prom.Counter({
|
||||||
|
name: 'misskey_authentication_failure_total',
|
||||||
|
help: 'Total number of authentication failures',
|
||||||
|
labelNames: ['cred_ty'],
|
||||||
|
});
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(DI.usersRepository)
|
@Inject(DI.usersRepository)
|
||||||
private usersRepository: UsersRepository,
|
private usersRepository: UsersRepository,
|
||||||
|
@ -51,6 +58,7 @@ export class AuthenticateService implements OnApplicationShutdown {
|
||||||
() => this.usersRepository.findOneBy({ token }) as Promise<MiLocalUser | null>);
|
() => this.usersRepository.findOneBy({ token }) as Promise<MiLocalUser | null>);
|
||||||
|
|
||||||
if (user == null) {
|
if (user == null) {
|
||||||
|
this.mAuthenticationFailureCounter.inc({ cred_ty: 'native' });
|
||||||
throw new AuthenticationError('user not found');
|
throw new AuthenticationError('user not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -65,6 +73,7 @@ export class AuthenticateService implements OnApplicationShutdown {
|
||||||
});
|
});
|
||||||
|
|
||||||
if (accessToken == null) {
|
if (accessToken == null) {
|
||||||
|
this.mAuthenticationFailureCounter.inc({ cred_ty: 'access_token' });
|
||||||
throw new AuthenticationError('invalid signature');
|
throw new AuthenticationError('invalid signature');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
52
packages/backend/src/server/api/MetricsService.ts
Normal file
52
packages/backend/src/server/api/MetricsService.ts
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
import { Inject, Injectable } from "@nestjs/common";
|
||||||
|
import * as prom from 'prom-client';
|
||||||
|
import { DI } from '@/di-symbols.js';
|
||||||
|
import type { Config } from '@/config.js';
|
||||||
|
import { bindThis } from "@/decorators.js";
|
||||||
|
import type { FastifyInstance, FastifyPluginOptions } from "fastify";
|
||||||
|
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project and yumechi
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class MetricsService {
|
||||||
|
constructor(
|
||||||
|
@Inject(DI.config)
|
||||||
|
private config: Config
|
||||||
|
) {}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public createServer(fastify: FastifyInstance, options: FastifyPluginOptions, done: (err?: Error) => void) {
|
||||||
|
if (this.config.prometheusMetrics?.enable) {
|
||||||
|
const token = this.config.prometheusMetrics.scrapeToken;
|
||||||
|
fastify.get('/metrics', async (request, reply) => {
|
||||||
|
if (token) {
|
||||||
|
const bearer = request.headers.authorization;
|
||||||
|
|
||||||
|
if (!bearer) {
|
||||||
|
reply.code(401);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [type, t] = bearer.split(' ');
|
||||||
|
|
||||||
|
if (type !== 'Bearer' || t !== token) {
|
||||||
|
reply.code(403);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
reply.header('Content-Type', prom.register.contentType);
|
||||||
|
reply.send(await prom.register.metrics());
|
||||||
|
} catch (err) {
|
||||||
|
reply.code(500);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
done();
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,6 +6,7 @@
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import bcrypt from 'bcryptjs';
|
import bcrypt from 'bcryptjs';
|
||||||
import { IsNull } from 'typeorm';
|
import { IsNull } from 'typeorm';
|
||||||
|
import * as prom from 'prom-client';
|
||||||
import * as Misskey from 'misskey-js';
|
import * as Misskey from 'misskey-js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import type {
|
import type {
|
||||||
|
@ -31,6 +32,12 @@ import type { FastifyReply, FastifyRequest } from 'fastify';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class SigninApiService {
|
export class SigninApiService {
|
||||||
|
private mSigninFailureCounter = new prom.Counter({
|
||||||
|
name: 'misskey_misskey_signin_failure',
|
||||||
|
help: 'The number of failed sign-ins',
|
||||||
|
labelNames: ['reason'],
|
||||||
|
});
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(DI.config)
|
@Inject(DI.config)
|
||||||
private config: Config,
|
private config: Config,
|
||||||
|
@ -93,6 +100,7 @@ export class SigninApiService {
|
||||||
// not more than 1 attempt per second and not more than 10 attempts per hour
|
// not more than 1 attempt per second and not more than 10 attempts per hour
|
||||||
await this.rateLimiterService.limit({ key: 'signin', duration: 60 * 60 * 1000, max: 10, minInterval: 1000 }, getIpHash(request.ip));
|
await this.rateLimiterService.limit({ key: 'signin', duration: 60 * 60 * 1000, max: 10, minInterval: 1000 }, getIpHash(request.ip));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
this.mSigninFailureCounter.inc({ reason: 'rate_limit' });
|
||||||
reply.code(429);
|
reply.code(429);
|
||||||
return {
|
return {
|
||||||
error: {
|
error: {
|
||||||
|
@ -104,11 +112,13 @@ export class SigninApiService {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof username !== 'string') {
|
if (typeof username !== 'string') {
|
||||||
|
this.mSigninFailureCounter.inc({ reason: 'bad_form' });
|
||||||
reply.code(400);
|
reply.code(400);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (token != null && typeof token !== 'string') {
|
if (token != null && typeof token !== 'string') {
|
||||||
|
this.mSigninFailureCounter.inc({ reason: 'bad_form' });
|
||||||
reply.code(400);
|
reply.code(400);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -120,12 +130,14 @@ export class SigninApiService {
|
||||||
}) as MiLocalUser;
|
}) as MiLocalUser;
|
||||||
|
|
||||||
if (user == null) {
|
if (user == null) {
|
||||||
|
this.mSigninFailureCounter.inc({ reason: 'user_not_found' });
|
||||||
return error(404, {
|
return error(404, {
|
||||||
id: '6cc579cc-885d-43d8-95c2-b8c7fc963280',
|
id: '6cc579cc-885d-43d8-95c2-b8c7fc963280',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (user.isSuspended) {
|
if (user.isSuspended) {
|
||||||
|
this.mSigninFailureCounter.inc({ reason: 'user_suspended' });
|
||||||
return error(403, {
|
return error(403, {
|
||||||
id: 'e03a5f46-d309-4865-9b69-56282d94e1eb',
|
id: 'e03a5f46-d309-4865-9b69-56282d94e1eb',
|
||||||
});
|
});
|
||||||
|
@ -150,6 +162,7 @@ export class SigninApiService {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof password !== 'string') {
|
if (typeof password !== 'string') {
|
||||||
|
this.mSigninFailureCounter.inc({ reason: 'bad_form' });
|
||||||
reply.code(400);
|
reply.code(400);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -167,6 +180,7 @@ export class SigninApiService {
|
||||||
success: false,
|
success: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.mSigninFailureCounter.inc({ reason: failure?.id ?? `unknown_error_${status ?? 500}` });
|
||||||
return error(status ?? 500, failure ?? { id: '4e30e80c-e338-45a0-8c8f-44455efa3b76' });
|
return error(status ?? 500, failure ?? { id: '4e30e80c-e338-45a0-8c8f-44455efa3b76' });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -174,30 +188,35 @@ export class SigninApiService {
|
||||||
if (process.env.NODE_ENV !== 'test') {
|
if (process.env.NODE_ENV !== 'test') {
|
||||||
if (this.meta.enableHcaptcha && this.meta.hcaptchaSecretKey) {
|
if (this.meta.enableHcaptcha && this.meta.hcaptchaSecretKey) {
|
||||||
await this.captchaService.verifyHcaptcha(this.meta.hcaptchaSecretKey, body['hcaptcha-response']).catch(err => {
|
await this.captchaService.verifyHcaptcha(this.meta.hcaptchaSecretKey, body['hcaptcha-response']).catch(err => {
|
||||||
|
this.mSigninFailureCounter.inc({ reason: 'captcha_verification_failed_hcaptcha' });
|
||||||
throw new FastifyReplyError(400, err);
|
throw new FastifyReplyError(400, err);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.meta.enableMcaptcha && this.meta.mcaptchaSecretKey && this.meta.mcaptchaSitekey && this.meta.mcaptchaInstanceUrl) {
|
if (this.meta.enableMcaptcha && this.meta.mcaptchaSecretKey && this.meta.mcaptchaSitekey && this.meta.mcaptchaInstanceUrl) {
|
||||||
await this.captchaService.verifyMcaptcha(this.meta.mcaptchaSecretKey, this.meta.mcaptchaSitekey, this.meta.mcaptchaInstanceUrl, body['m-captcha-response']).catch(err => {
|
await this.captchaService.verifyMcaptcha(this.meta.mcaptchaSecretKey, this.meta.mcaptchaSitekey, this.meta.mcaptchaInstanceUrl, body['m-captcha-response']).catch(err => {
|
||||||
|
this.mSigninFailureCounter.inc({ reason: 'captcha_verification_failed_mcaptcha' });
|
||||||
throw new FastifyReplyError(400, err);
|
throw new FastifyReplyError(400, err);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.meta.enableRecaptcha && this.meta.recaptchaSecretKey) {
|
if (this.meta.enableRecaptcha && this.meta.recaptchaSecretKey) {
|
||||||
await this.captchaService.verifyRecaptcha(this.meta.recaptchaSecretKey, body['g-recaptcha-response']).catch(err => {
|
await this.captchaService.verifyRecaptcha(this.meta.recaptchaSecretKey, body['g-recaptcha-response']).catch(err => {
|
||||||
|
this.mSigninFailureCounter.inc({ reason: 'captcha_verification_failed_recaptcha' });
|
||||||
throw new FastifyReplyError(400, err);
|
throw new FastifyReplyError(400, err);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.meta.enableTurnstile && this.meta.turnstileSecretKey) {
|
if (this.meta.enableTurnstile && this.meta.turnstileSecretKey) {
|
||||||
await this.captchaService.verifyTurnstile(this.meta.turnstileSecretKey, body['turnstile-response']).catch(err => {
|
await this.captchaService.verifyTurnstile(this.meta.turnstileSecretKey, body['turnstile-response']).catch(err => {
|
||||||
|
this.mSigninFailureCounter.inc({ reason: 'captcha_verification_failed_turnstile' });
|
||||||
throw new FastifyReplyError(400, err);
|
throw new FastifyReplyError(400, err);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.meta.enableTestcaptcha) {
|
if (this.meta.enableTestcaptcha) {
|
||||||
await this.captchaService.verifyTestcaptcha(body['testcaptcha-response']).catch(err => {
|
await this.captchaService.verifyTestcaptcha(body['testcaptcha-response']).catch(err => {
|
||||||
|
this.mSigninFailureCounter.inc({ reason: 'captcha_verification_failed_testcaptcha' });
|
||||||
throw new FastifyReplyError(400, err);
|
throw new FastifyReplyError(400, err);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -167,7 +167,7 @@ export function federationInstance(): entities.FederationInstance {
|
||||||
notesCount: 20,
|
notesCount: 20,
|
||||||
followingCount: 5,
|
followingCount: 5,
|
||||||
followersCount: 15,
|
followersCount: 15,
|
||||||
isNotResponding: false,
|
: false,
|
||||||
isSuspended: false,
|
isSuspended: false,
|
||||||
suspensionState: 'none',
|
suspensionState: 'none',
|
||||||
isBlocked: false,
|
isBlocked: false,
|
||||||
|
|
|
@ -1812,10 +1812,6 @@ declare namespace entities {
|
||||||
MeDetailed,
|
MeDetailed,
|
||||||
UserDetailed,
|
UserDetailed,
|
||||||
User,
|
User,
|
||||||
UserWebhookBody,
|
|
||||||
UserWebhookNoteBody,
|
|
||||||
UserWebhookUserBody,
|
|
||||||
UserWebhookReactionBody,
|
|
||||||
UserList,
|
UserList,
|
||||||
Ad,
|
Ad,
|
||||||
Announcement,
|
Announcement,
|
||||||
|
@ -3414,18 +3410,6 @@ type UsersShowResponse = operations['users___show']['responses']['200']['content
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
type UsersUpdateMemoRequest = operations['users___update-memo']['requestBody']['content']['application/json'];
|
type UsersUpdateMemoRequest = operations['users___update-memo']['requestBody']['content']['application/json'];
|
||||||
|
|
||||||
// @public (undocumented)
|
|
||||||
type UserWebhookBody = components['schemas']['UserWebhookBody'];
|
|
||||||
|
|
||||||
// @public (undocumented)
|
|
||||||
type UserWebhookNoteBody = components['schemas']['UserWebhookNoteBody'];
|
|
||||||
|
|
||||||
// @public (undocumented)
|
|
||||||
type UserWebhookReactionBody = components['schemas']['UserWebhookReactionBody'];
|
|
||||||
|
|
||||||
// @public (undocumented)
|
|
||||||
type UserWebhookUserBody = components['schemas']['UserWebhookUserBody'];
|
|
||||||
|
|
||||||
// Warnings were encountered during analysis:
|
// Warnings were encountered during analysis:
|
||||||
//
|
//
|
||||||
// src/entities.ts:50:2 - (ae-forgotten-export) The symbol "ModerationLogPayloads" needs to be exported by the entry point index.d.ts
|
// src/entities.ts:50:2 - (ae-forgotten-export) The symbol "ModerationLogPayloads" needs to be exported by the entry point index.d.ts
|
||||||
|
|
|
@ -7,10 +7,6 @@ export type UserDetailedNotMe = components['schemas']['UserDetailedNotMe'];
|
||||||
export type MeDetailed = components['schemas']['MeDetailed'];
|
export type MeDetailed = components['schemas']['MeDetailed'];
|
||||||
export type UserDetailed = components['schemas']['UserDetailed'];
|
export type UserDetailed = components['schemas']['UserDetailed'];
|
||||||
export type User = components['schemas']['User'];
|
export type User = components['schemas']['User'];
|
||||||
export type UserWebhookBody = components['schemas']['UserWebhookBody'];
|
|
||||||
export type UserWebhookNoteBody = components['schemas']['UserWebhookNoteBody'];
|
|
||||||
export type UserWebhookUserBody = components['schemas']['UserWebhookUserBody'];
|
|
||||||
export type UserWebhookReactionBody = components['schemas']['UserWebhookReactionBody'];
|
|
||||||
export type UserList = components['schemas']['UserList'];
|
export type UserList = components['schemas']['UserList'];
|
||||||
export type Ad = components['schemas']['Ad'];
|
export type Ad = components['schemas']['Ad'];
|
||||||
export type Announcement = components['schemas']['Announcement'];
|
export type Announcement = components['schemas']['Announcement'];
|
||||||
|
|
|
@ -4022,32 +4022,6 @@ export type components = {
|
||||||
MeDetailed: components['schemas']['UserLite'] & components['schemas']['UserDetailedNotMeOnly'] & components['schemas']['MeDetailedOnly'];
|
MeDetailed: components['schemas']['UserLite'] & components['schemas']['UserDetailedNotMeOnly'] & components['schemas']['MeDetailedOnly'];
|
||||||
UserDetailed: components['schemas']['UserDetailedNotMe'] | components['schemas']['MeDetailed'];
|
UserDetailed: components['schemas']['UserDetailedNotMe'] | components['schemas']['MeDetailed'];
|
||||||
User: components['schemas']['UserLite'] | components['schemas']['UserDetailed'];
|
User: components['schemas']['UserLite'] | components['schemas']['UserDetailed'];
|
||||||
UserWebhookBody: OneOf<[{
|
|
||||||
note: components['schemas']['Note'];
|
|
||||||
}, {
|
|
||||||
user: components['schemas']['UserLite'];
|
|
||||||
}, {
|
|
||||||
note: components['schemas']['Note'];
|
|
||||||
reaction: {
|
|
||||||
id: string;
|
|
||||||
user: components['schemas']['UserLite'];
|
|
||||||
reaction: string;
|
|
||||||
};
|
|
||||||
}]>;
|
|
||||||
UserWebhookNoteBody: {
|
|
||||||
note: components['schemas']['Note'];
|
|
||||||
};
|
|
||||||
UserWebhookUserBody: {
|
|
||||||
user: components['schemas']['UserLite'];
|
|
||||||
};
|
|
||||||
UserWebhookReactionBody: {
|
|
||||||
note: components['schemas']['Note'];
|
|
||||||
reaction: {
|
|
||||||
id: string;
|
|
||||||
user: components['schemas']['UserLite'];
|
|
||||||
reaction: string;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
UserList: {
|
UserList: {
|
||||||
/**
|
/**
|
||||||
* Format: id
|
* Format: id
|
||||||
|
|
|
@ -350,6 +350,9 @@ importers:
|
||||||
probe-image-size:
|
probe-image-size:
|
||||||
specifier: 7.2.3
|
specifier: 7.2.3
|
||||||
version: 7.2.3
|
version: 7.2.3
|
||||||
|
prom-client:
|
||||||
|
specifier: ^15.1.3
|
||||||
|
version: 15.1.3
|
||||||
promise-limit:
|
promise-limit:
|
||||||
specifier: 2.7.0
|
specifier: 2.7.0
|
||||||
version: 2.7.0
|
version: 2.7.0
|
||||||
|
@ -5252,6 +5255,9 @@ packages:
|
||||||
resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==}
|
resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
bintrees@1.0.2:
|
||||||
|
resolution: {integrity: sha512-VOMgTMwjAaUG580SXn3LacVgjurrbMme7ZZNYGSSV7mmtY6QQRh0Eg3pwIcntQ77DErK1L0NxkbetjcoXzVwKw==}
|
||||||
|
|
||||||
blob-util@2.0.2:
|
blob-util@2.0.2:
|
||||||
resolution: {integrity: sha512-T7JQa+zsXXEa6/8ZhHcQEW1UFfVM49Ts65uBkFL6fz2QmrElqmbajIDJvuA0tEhRe5eIjpV9ZF+0RfZR9voJFQ==}
|
resolution: {integrity: sha512-T7JQa+zsXXEa6/8ZhHcQEW1UFfVM49Ts65uBkFL6fz2QmrElqmbajIDJvuA0tEhRe5eIjpV9ZF+0RfZR9voJFQ==}
|
||||||
|
|
||||||
|
@ -9329,6 +9335,10 @@ packages:
|
||||||
resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==}
|
resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==}
|
||||||
engines: {node: '>=0.4.0'}
|
engines: {node: '>=0.4.0'}
|
||||||
|
|
||||||
|
prom-client@15.1.3:
|
||||||
|
resolution: {integrity: sha512-6ZiOBfCywsD4k1BN9IX0uZhF+tJkV8q8llP64G5Hajs4JOeVLPCwpPVcpXy3BwYiUGgyJzsJJQeOIv7+hDSq8g==}
|
||||||
|
engines: {node: ^16 || ^18 || >=20}
|
||||||
|
|
||||||
promise-limit@2.7.0:
|
promise-limit@2.7.0:
|
||||||
resolution: {integrity: sha512-7nJ6v5lnJsXwGprnGXga4wx6d1POjvi5Qmf1ivTRxTjH4Z/9Czja/UCMLVmB9N93GeWOU93XaFaEt6jbuoagNw==}
|
resolution: {integrity: sha512-7nJ6v5lnJsXwGprnGXga4wx6d1POjvi5Qmf1ivTRxTjH4Z/9Czja/UCMLVmB9N93GeWOU93XaFaEt6jbuoagNw==}
|
||||||
|
|
||||||
|
@ -10345,6 +10355,9 @@ packages:
|
||||||
resolution: {integrity: sha512-+HRtZ40Vc+6YfCDWCeAsixwxJgMbPY4HHuTgzPYH3JXvqHWUlsCfy+ylXlAKhFNcuLp4xVeWeFBUhDk+7KYUvQ==}
|
resolution: {integrity: sha512-+HRtZ40Vc+6YfCDWCeAsixwxJgMbPY4HHuTgzPYH3JXvqHWUlsCfy+ylXlAKhFNcuLp4xVeWeFBUhDk+7KYUvQ==}
|
||||||
engines: {node: '>=14.16'}
|
engines: {node: '>=14.16'}
|
||||||
|
|
||||||
|
tdigest@0.1.2:
|
||||||
|
resolution: {integrity: sha512-+G0LLgjjo9BZX2MfdvPfH+MKLCrxlXSYec5DaPYP1fe6Iyhf0/fSmJ0bFiZ1F8BT6cGXl2LpltQptzjXKWEkKA==}
|
||||||
|
|
||||||
terser@5.36.0:
|
terser@5.36.0:
|
||||||
resolution: {integrity: sha512-IYV9eNMuFAV4THUspIRXkLakHnV6XO7FEdtKjf/mDyrnqUg9LnlOn6/RwRvM9SZjR4GUq8Nk8zj67FzVARr74w==}
|
resolution: {integrity: sha512-IYV9eNMuFAV4THUspIRXkLakHnV6XO7FEdtKjf/mDyrnqUg9LnlOn6/RwRvM9SZjR4GUq8Nk8zj67FzVARr74w==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
@ -16086,6 +16099,8 @@ snapshots:
|
||||||
|
|
||||||
binary-extensions@2.2.0: {}
|
binary-extensions@2.2.0: {}
|
||||||
|
|
||||||
|
bintrees@1.0.2: {}
|
||||||
|
|
||||||
blob-util@2.0.2: {}
|
blob-util@2.0.2: {}
|
||||||
|
|
||||||
bluebird@3.7.2: {}
|
bluebird@3.7.2: {}
|
||||||
|
@ -21097,6 +21112,11 @@ snapshots:
|
||||||
progress@2.0.3:
|
progress@2.0.3:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
prom-client@15.1.3:
|
||||||
|
dependencies:
|
||||||
|
'@opentelemetry/api': 1.9.0
|
||||||
|
tdigest: 0.1.2
|
||||||
|
|
||||||
promise-limit@2.7.0: {}
|
promise-limit@2.7.0: {}
|
||||||
|
|
||||||
promise-polyfill@8.3.0: {}
|
promise-polyfill@8.3.0: {}
|
||||||
|
@ -22280,6 +22300,10 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
execa: 6.1.0
|
execa: 6.1.0
|
||||||
|
|
||||||
|
tdigest@0.1.2:
|
||||||
|
dependencies:
|
||||||
|
bintrees: 1.0.2
|
||||||
|
|
||||||
terser@5.36.0:
|
terser@5.36.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@jridgewell/source-map': 0.3.6
|
'@jridgewell/source-map': 0.3.6
|
||||||
|
|
18
yume-mods/etc/config.alloy
Normal file
18
yume-mods/etc/config.alloy
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
prometheus.scrape "yumechinokuni" {
|
||||||
|
targets = [
|
||||||
|
{
|
||||||
|
__address__ = "mi.yumechi.jp:443",
|
||||||
|
__scheme__ = "https",
|
||||||
|
environment = "prod",
|
||||||
|
hostname = "mi.yumechi.jp",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
__address__ = "test0.mi.yumechi.jp:443",
|
||||||
|
__scheme__ = "https",
|
||||||
|
environment = "test",
|
||||||
|
hostname = "test0.mi.yumechi.jp",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
forward_to = [prometheus.remote_write.mihari.receiver]
|
||||||
|
}
|
Loading…
Reference in a new issue