2023-07-27 00:31:52 -05:00
|
|
|
/*
|
2024-02-13 09:59:27 -06:00
|
|
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
2023-07-27 00:31:52 -05:00
|
|
|
* SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
*/
|
|
|
|
|
2022-09-17 13:27:08 -05:00
|
|
|
// https://github.com/typeorm/typeorm/issues/2400
|
|
|
|
import pg from 'pg';
|
2022-09-23 20:45:42 -05:00
|
|
|
import { DataSource, Logger } from 'typeorm';
|
2022-09-17 13:27:08 -05:00
|
|
|
import * as highlight from 'cli-highlight';
|
2024-11-16 23:00:06 -06:00
|
|
|
import { createHash } from 'crypto';
|
2022-09-17 13:27:08 -05:00
|
|
|
import { entities as charts } from '@/core/chart/entities.js';
|
|
|
|
|
2023-09-19 21:33:36 -05:00
|
|
|
import { MiAbuseUserReport } from '@/models/AbuseUserReport.js';
|
2024-06-08 01:34:19 -05:00
|
|
|
import { MiAbuseReportNotificationRecipient } from '@/models/AbuseReportNotificationRecipient.js';
|
2023-09-19 21:33:36 -05:00
|
|
|
import { MiAccessToken } from '@/models/AccessToken.js';
|
|
|
|
import { MiAd } from '@/models/Ad.js';
|
|
|
|
import { MiAnnouncement } from '@/models/Announcement.js';
|
|
|
|
import { MiAnnouncementRead } from '@/models/AnnouncementRead.js';
|
|
|
|
import { MiAntenna } from '@/models/Antenna.js';
|
|
|
|
import { MiApp } from '@/models/App.js';
|
2023-10-21 04:38:07 -05:00
|
|
|
import { MiAvatarDecoration } from '@/models/AvatarDecoration.js';
|
2023-09-19 21:33:36 -05:00
|
|
|
import { MiAuthSession } from '@/models/AuthSession.js';
|
|
|
|
import { MiBlocking } from '@/models/Blocking.js';
|
|
|
|
import { MiChannelFollowing } from '@/models/ChannelFollowing.js';
|
|
|
|
import { MiChannelFavorite } from '@/models/ChannelFavorite.js';
|
|
|
|
import { MiClip } from '@/models/Clip.js';
|
|
|
|
import { MiClipNote } from '@/models/ClipNote.js';
|
|
|
|
import { MiClipFavorite } from '@/models/ClipFavorite.js';
|
|
|
|
import { MiDriveFile } from '@/models/DriveFile.js';
|
|
|
|
import { MiDriveFolder } from '@/models/DriveFolder.js';
|
|
|
|
import { MiEmoji } from '@/models/Emoji.js';
|
|
|
|
import { MiFollowing } from '@/models/Following.js';
|
|
|
|
import { MiFollowRequest } from '@/models/FollowRequest.js';
|
|
|
|
import { MiGalleryLike } from '@/models/GalleryLike.js';
|
|
|
|
import { MiGalleryPost } from '@/models/GalleryPost.js';
|
|
|
|
import { MiHashtag } from '@/models/Hashtag.js';
|
|
|
|
import { MiInstance } from '@/models/Instance.js';
|
|
|
|
import { MiMeta } from '@/models/Meta.js';
|
|
|
|
import { MiModerationLog } from '@/models/ModerationLog.js';
|
|
|
|
import { MiMuting } from '@/models/Muting.js';
|
|
|
|
import { MiRenoteMuting } from '@/models/RenoteMuting.js';
|
|
|
|
import { MiNote } from '@/models/Note.js';
|
|
|
|
import { MiNoteFavorite } from '@/models/NoteFavorite.js';
|
|
|
|
import { MiNoteReaction } from '@/models/NoteReaction.js';
|
|
|
|
import { MiNoteThreadMuting } from '@/models/NoteThreadMuting.js';
|
|
|
|
import { MiNoteUnread } from '@/models/NoteUnread.js';
|
|
|
|
import { MiPage } from '@/models/Page.js';
|
|
|
|
import { MiPageLike } from '@/models/PageLike.js';
|
|
|
|
import { MiPasswordResetRequest } from '@/models/PasswordResetRequest.js';
|
|
|
|
import { MiPoll } from '@/models/Poll.js';
|
|
|
|
import { MiPollVote } from '@/models/PollVote.js';
|
|
|
|
import { MiPromoNote } from '@/models/PromoNote.js';
|
|
|
|
import { MiPromoRead } from '@/models/PromoRead.js';
|
|
|
|
import { MiRegistrationTicket } from '@/models/RegistrationTicket.js';
|
|
|
|
import { MiRegistryItem } from '@/models/RegistryItem.js';
|
|
|
|
import { MiRelay } from '@/models/Relay.js';
|
|
|
|
import { MiSignin } from '@/models/Signin.js';
|
|
|
|
import { MiSwSubscription } from '@/models/SwSubscription.js';
|
|
|
|
import { MiUsedUsername } from '@/models/UsedUsername.js';
|
|
|
|
import { MiUser } from '@/models/User.js';
|
|
|
|
import { MiUserIp } from '@/models/UserIp.js';
|
|
|
|
import { MiUserKeypair } from '@/models/UserKeypair.js';
|
|
|
|
import { MiUserList } from '@/models/UserList.js';
|
|
|
|
import { MiUserListFavorite } from '@/models/UserListFavorite.js';
|
2023-10-03 06:26:11 -05:00
|
|
|
import { MiUserListMembership } from '@/models/UserListMembership.js';
|
2023-09-19 21:33:36 -05:00
|
|
|
import { MiUserNotePining } from '@/models/UserNotePining.js';
|
|
|
|
import { MiUserPending } from '@/models/UserPending.js';
|
|
|
|
import { MiUserProfile } from '@/models/UserProfile.js';
|
|
|
|
import { MiUserPublickey } from '@/models/UserPublickey.js';
|
|
|
|
import { MiUserSecurityKey } from '@/models/UserSecurityKey.js';
|
|
|
|
import { MiWebhook } from '@/models/Webhook.js';
|
2024-06-08 01:34:19 -05:00
|
|
|
import { MiSystemWebhook } from '@/models/SystemWebhook.js';
|
2023-09-19 21:33:36 -05:00
|
|
|
import { MiChannel } from '@/models/Channel.js';
|
|
|
|
import { MiRetentionAggregation } from '@/models/RetentionAggregation.js';
|
|
|
|
import { MiRole } from '@/models/Role.js';
|
|
|
|
import { MiRoleAssignment } from '@/models/RoleAssignment.js';
|
|
|
|
import { MiFlash } from '@/models/Flash.js';
|
|
|
|
import { MiFlashLike } from '@/models/FlashLike.js';
|
|
|
|
import { MiUserMemo } from '@/models/UserMemo.js';
|
2024-01-11 03:13:39 -06:00
|
|
|
import { MiBubbleGameRecord } from '@/models/BubbleGameRecord.js';
|
2024-01-19 05:51:49 -06:00
|
|
|
import { MiReversiGame } from '@/models/ReversiGame.js';
|
2022-09-17 13:27:08 -05:00
|
|
|
|
2022-09-21 15:11:26 -05:00
|
|
|
import { Config } from '@/config.js';
|
2022-09-23 20:45:42 -05:00
|
|
|
import MisskeyLogger from '@/logger.js';
|
2022-12-04 00:03:09 -06:00
|
|
|
import { bindThis } from '@/decorators.js';
|
2024-11-16 23:00:06 -06:00
|
|
|
import { MemoryKVCache } from './misc/cache.js';
|
2024-11-17 06:01:25 -06:00
|
|
|
import { metricCounter, metricHistogram } from './server/api/MetricsService.js';
|
2022-09-17 13:27:08 -05:00
|
|
|
|
2024-06-08 01:34:19 -05:00
|
|
|
pg.types.setTypeParser(20, Number);
|
|
|
|
|
2022-09-23 20:45:42 -05:00
|
|
|
export const dbLogger = new MisskeyLogger('db');
|
2022-09-17 13:27:08 -05:00
|
|
|
|
2024-06-05 21:40:11 -05:00
|
|
|
const sqlLogger = dbLogger.createSubLogger('sql', 'gray');
|
2022-09-17 13:27:08 -05:00
|
|
|
|
2024-11-16 23:00:06 -06:00
|
|
|
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'),
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2024-11-17 06:01:25 -06:00
|
|
|
const mQueryCounter = metricCounter({
|
2024-11-16 23:00:06 -06:00
|
|
|
name: 'misskey_postgres_query_total',
|
|
|
|
help: 'Total queries to postgres',
|
|
|
|
labelNames: ['join', 'from', 'hash'],
|
|
|
|
});
|
|
|
|
|
2024-11-17 06:01:25 -06:00
|
|
|
const mQueryErrorCounter = metricCounter({
|
2024-11-16 23:00:06 -06:00
|
|
|
name: 'misskey_postgres_query_error_total',
|
|
|
|
help: 'Total errors in queries to postgres',
|
|
|
|
labelNames: ['join', 'from', 'hash'],
|
|
|
|
});
|
|
|
|
|
2024-11-17 06:01:25 -06:00
|
|
|
const mSlowQueryHisto = metricHistogram({
|
2024-11-16 23:00:06 -06:00
|
|
|
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],
|
|
|
|
});
|
|
|
|
|
2022-09-17 13:27:08 -05:00
|
|
|
class MyCustomLogger implements Logger {
|
2024-11-16 23:00:06 -06:00
|
|
|
constructor(private metricOnly = true) {}
|
|
|
|
|
|
|
|
private queryHashCache = new MemoryKVCache<QueryTagCache>(1000 * 60 * 5); // 5m
|
|
|
|
|
2022-12-04 00:03:09 -06:00
|
|
|
@bindThis
|
2022-09-17 13:27:08 -05:00
|
|
|
private highlight(sql: string) {
|
|
|
|
return highlight.highlight(sql, {
|
|
|
|
language: 'sql', ignoreIllegals: true,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2024-11-16 23:00:06 -06:00
|
|
|
@bindThis
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2022-12-04 00:03:09 -06:00
|
|
|
@bindThis
|
2022-09-17 13:27:08 -05:00
|
|
|
public logQuery(query: string, parameters?: any[]) {
|
2024-11-17 06:01:25 -06:00
|
|
|
mQueryCounter?.inc(this.getQueryTags(query));
|
2024-11-16 23:00:06 -06:00
|
|
|
|
|
|
|
if (this.metricOnly) {
|
|
|
|
return;
|
|
|
|
}
|
2022-09-17 13:27:08 -05:00
|
|
|
sqlLogger.info(this.highlight(query).substring(0, 100));
|
|
|
|
}
|
|
|
|
|
2022-12-04 00:03:09 -06:00
|
|
|
@bindThis
|
2022-09-17 13:27:08 -05:00
|
|
|
public logQueryError(error: string, query: string, parameters?: any[]) {
|
2024-11-17 06:01:25 -06:00
|
|
|
mQueryErrorCounter?.inc(this.getQueryTags(query));
|
2024-11-16 23:00:06 -06:00
|
|
|
|
|
|
|
if (this.metricOnly) {
|
|
|
|
return;
|
|
|
|
}
|
2022-09-17 13:27:08 -05:00
|
|
|
sqlLogger.error(this.highlight(query));
|
|
|
|
}
|
|
|
|
|
2022-12-04 00:03:09 -06:00
|
|
|
@bindThis
|
2022-09-17 13:27:08 -05:00
|
|
|
public logQuerySlow(time: number, query: string, parameters?: any[]) {
|
2024-11-17 06:01:25 -06:00
|
|
|
mSlowQueryHisto?.observe(this.getQueryTags(query), time);
|
2024-11-16 23:00:06 -06:00
|
|
|
|
|
|
|
if (this.metricOnly) {
|
|
|
|
return;
|
|
|
|
}
|
2022-09-17 13:27:08 -05:00
|
|
|
sqlLogger.warn(this.highlight(query));
|
|
|
|
}
|
|
|
|
|
2022-12-04 00:03:09 -06:00
|
|
|
@bindThis
|
2022-09-17 13:27:08 -05:00
|
|
|
public logSchemaBuild(message: string) {
|
2024-11-16 23:00:06 -06:00
|
|
|
if (this.metricOnly) {
|
|
|
|
return;
|
|
|
|
}
|
2022-09-17 13:27:08 -05:00
|
|
|
sqlLogger.info(message);
|
|
|
|
}
|
|
|
|
|
2022-12-04 00:03:09 -06:00
|
|
|
@bindThis
|
2022-09-17 13:27:08 -05:00
|
|
|
public log(message: string) {
|
2024-11-16 23:00:06 -06:00
|
|
|
if (this.metricOnly) {
|
|
|
|
return;
|
|
|
|
}
|
2022-09-17 13:27:08 -05:00
|
|
|
sqlLogger.info(message);
|
|
|
|
}
|
|
|
|
|
2022-12-04 00:03:09 -06:00
|
|
|
@bindThis
|
2022-09-17 13:27:08 -05:00
|
|
|
public logMigration(message: string) {
|
2024-11-16 23:00:06 -06:00
|
|
|
if (this.metricOnly) {
|
|
|
|
return;
|
|
|
|
}
|
2022-09-17 13:27:08 -05:00
|
|
|
sqlLogger.info(message);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export const entities = [
|
2023-08-16 03:51:28 -05:00
|
|
|
MiAnnouncement,
|
|
|
|
MiAnnouncementRead,
|
|
|
|
MiMeta,
|
|
|
|
MiInstance,
|
|
|
|
MiApp,
|
2023-10-21 04:38:07 -05:00
|
|
|
MiAvatarDecoration,
|
2023-08-16 03:51:28 -05:00
|
|
|
MiAuthSession,
|
|
|
|
MiAccessToken,
|
|
|
|
MiUser,
|
|
|
|
MiUserProfile,
|
|
|
|
MiUserKeypair,
|
|
|
|
MiUserPublickey,
|
|
|
|
MiUserList,
|
|
|
|
MiUserListFavorite,
|
2023-10-03 06:26:11 -05:00
|
|
|
MiUserListMembership,
|
2023-08-16 03:51:28 -05:00
|
|
|
MiUserNotePining,
|
|
|
|
MiUserSecurityKey,
|
|
|
|
MiUsedUsername,
|
|
|
|
MiFollowing,
|
|
|
|
MiFollowRequest,
|
|
|
|
MiMuting,
|
|
|
|
MiRenoteMuting,
|
|
|
|
MiBlocking,
|
|
|
|
MiNote,
|
|
|
|
MiNoteFavorite,
|
|
|
|
MiNoteReaction,
|
|
|
|
MiNoteThreadMuting,
|
|
|
|
MiNoteUnread,
|
|
|
|
MiPage,
|
|
|
|
MiPageLike,
|
|
|
|
MiGalleryPost,
|
|
|
|
MiGalleryLike,
|
|
|
|
MiDriveFile,
|
|
|
|
MiDriveFolder,
|
|
|
|
MiPoll,
|
|
|
|
MiPollVote,
|
|
|
|
MiEmoji,
|
|
|
|
MiHashtag,
|
|
|
|
MiSwSubscription,
|
|
|
|
MiAbuseUserReport,
|
2024-06-08 01:34:19 -05:00
|
|
|
MiAbuseReportNotificationRecipient,
|
2023-08-16 03:51:28 -05:00
|
|
|
MiRegistrationTicket,
|
|
|
|
MiSignin,
|
|
|
|
MiModerationLog,
|
|
|
|
MiClip,
|
|
|
|
MiClipNote,
|
|
|
|
MiClipFavorite,
|
|
|
|
MiAntenna,
|
|
|
|
MiPromoNote,
|
|
|
|
MiPromoRead,
|
|
|
|
MiRelay,
|
|
|
|
MiChannel,
|
|
|
|
MiChannelFollowing,
|
|
|
|
MiChannelFavorite,
|
|
|
|
MiRegistryItem,
|
|
|
|
MiAd,
|
|
|
|
MiPasswordResetRequest,
|
|
|
|
MiUserPending,
|
|
|
|
MiWebhook,
|
2024-06-08 01:34:19 -05:00
|
|
|
MiSystemWebhook,
|
2023-08-16 03:51:28 -05:00
|
|
|
MiUserIp,
|
|
|
|
MiRetentionAggregation,
|
|
|
|
MiRole,
|
|
|
|
MiRoleAssignment,
|
|
|
|
MiFlash,
|
|
|
|
MiFlashLike,
|
|
|
|
MiUserMemo,
|
2024-01-11 03:13:39 -06:00
|
|
|
MiBubbleGameRecord,
|
2024-01-19 05:51:49 -06:00
|
|
|
MiReversiGame,
|
2022-09-17 13:27:08 -05:00
|
|
|
...charts,
|
|
|
|
];
|
|
|
|
|
|
|
|
const log = process.env.NODE_ENV !== 'production';
|
|
|
|
|
2024-11-16 23:00:06 -06:00
|
|
|
export function createPostgresDataSource(config: Config, isMain = false) {
|
2022-09-17 13:27:08 -05:00
|
|
|
return new DataSource({
|
|
|
|
type: 'postgres',
|
|
|
|
host: config.db.host,
|
|
|
|
port: config.db.port,
|
|
|
|
username: config.db.user,
|
|
|
|
password: config.db.pass,
|
|
|
|
database: config.db.db,
|
|
|
|
extra: {
|
|
|
|
statement_timeout: 1000 * 10,
|
|
|
|
...config.db.extra,
|
|
|
|
},
|
2024-01-21 03:43:01 -06:00
|
|
|
...(config.dbReplications ? {
|
|
|
|
replication: {
|
|
|
|
master: {
|
|
|
|
host: config.db.host,
|
|
|
|
port: config.db.port,
|
|
|
|
username: config.db.user,
|
|
|
|
password: config.db.pass,
|
|
|
|
database: config.db.db,
|
|
|
|
},
|
|
|
|
slaves: config.dbSlaves!.map(rep => ({
|
|
|
|
host: rep.host,
|
|
|
|
port: rep.port,
|
|
|
|
username: rep.user,
|
|
|
|
password: rep.pass,
|
|
|
|
database: rep.db,
|
|
|
|
})),
|
2023-04-08 01:53:36 -05:00
|
|
|
},
|
2024-01-21 03:43:01 -06:00
|
|
|
} : {}),
|
2022-09-17 13:27:08 -05:00
|
|
|
synchronize: process.env.NODE_ENV === 'test',
|
|
|
|
dropSchema: process.env.NODE_ENV === 'test',
|
|
|
|
cache: !config.db.disableCache && process.env.NODE_ENV !== 'test' ? { // dbをcloseしても何故かredisのコネクションが内部的に残り続けるようで、テストの際に支障が出るため無効にする(キャッシュも含めてテストしたいため本当は有効にしたいが...)
|
|
|
|
type: 'ioredis',
|
|
|
|
options: {
|
|
|
|
host: config.redis.host,
|
|
|
|
port: config.redis.port,
|
2023-07-31 05:14:20 -05:00
|
|
|
family: config.redis.family ?? 0,
|
2022-09-17 13:27:08 -05:00
|
|
|
password: config.redis.pass,
|
|
|
|
keyPrefix: `${config.redis.prefix}:query:`,
|
|
|
|
db: config.redis.db ?? 0,
|
|
|
|
},
|
|
|
|
} : false,
|
2024-11-17 06:01:25 -06:00
|
|
|
logging: true,
|
|
|
|
logger: new MyCustomLogger(!log),
|
2024-11-16 23:00:06 -06:00
|
|
|
maxQueryExecutionTime: 500,
|
2022-09-17 13:27:08 -05:00
|
|
|
entities: entities,
|
|
|
|
migrations: ['../../migration/*.js'],
|
|
|
|
});
|
|
|
|
}
|