/* * SPDX-FileCopyrightText: syuilo and misskey-project * SPDX-License-Identifier: AGPL-3.0-only */ // https://github.com/typeorm/typeorm/issues/2400 import pg from 'pg'; import { DataSource, Logger } from 'typeorm'; import * as highlight from 'cli-highlight'; import { createHash } from 'crypto'; import { entities as charts } from '@/core/chart/entities.js'; import { MiAbuseUserReport } from '@/models/AbuseUserReport.js'; import { MiAbuseReportNotificationRecipient } from '@/models/AbuseReportNotificationRecipient.js'; 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'; import { MiAvatarDecoration } from '@/models/AvatarDecoration.js'; 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'; import { MiUserListMembership } from '@/models/UserListMembership.js'; 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'; import { MiSystemWebhook } from '@/models/SystemWebhook.js'; 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'; import { MiBubbleGameRecord } from '@/models/BubbleGameRecord.js'; import { MiReversiGame } from '@/models/ReversiGame.js'; import { Config } from '@/config.js'; import MisskeyLogger from '@/logger.js'; import { bindThis } from '@/decorators.js'; import { MemoryKVCache } from './misc/cache.js'; import { metricCounter, metricHistogram } from './server/api/MetricsService.js'; pg.types.setTypeParser(20, Number); export const dbLogger = new MisskeyLogger('db'); const sqlLogger = dbLogger.createSubLogger('sql', 'gray'); type QueryTagCache = { join: string; from: string; }; function dedupConsecutive(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)[\s\S]+JOIN[\s\r\n]+([a-zA-Z0-9_"`.]+)/ig); const froms = query.matchAll(/FROM[\s\r\n]+([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('|'); return { join, from, }; } const mQueryCounter = metricCounter({ name: 'misskey_postgres_query_total', help: 'Total queries to postgres', labelNames: ['join', 'from'], }); const mQueryErrorCounter = metricCounter({ name: 'misskey_postgres_query_error_total', help: 'Total errors in queries to postgres', labelNames: ['join', 'from'], }); const mSlowQueryHisto = metricHistogram({ name: 'misskey_postgres_query_slow_duration_seconds', help: 'Duration of slow queries to postgres', labelNames: ['join', 'from'], buckets: [0.1, 0.5, 1, 2, 5, 10, 30, 300], }); class MyCustomLogger implements Logger { constructor(private metricOnly = true) {} private queryHashCache = new MemoryKVCache(1000 * 60 * 5); // 5m @bindThis private highlight(sql: string) { return highlight.highlight(sql, { language: 'sql', ignoreIllegals: true, }); } @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; } @bindThis public logQuery(query: string, parameters?: any[]) { mQueryCounter?.inc(this.getQueryTags(query)); if (this.metricOnly) { return; } sqlLogger.info(this.highlight(query).substring(0, 100)); } @bindThis public logQueryError(error: string, query: string, parameters?: any[]) { mQueryErrorCounter?.inc(this.getQueryTags(query)); if (this.metricOnly) { return; } sqlLogger.error(this.highlight(query)); } @bindThis public logQuerySlow(time: number, query: string, parameters?: any[]) { mSlowQueryHisto?.observe(this.getQueryTags(query), time); if (this.metricOnly) { return; } sqlLogger.warn(this.highlight(query)); } @bindThis public logSchemaBuild(message: string) { if (this.metricOnly) { return; } sqlLogger.info(message); } @bindThis public log(message: string) { if (this.metricOnly) { return; } sqlLogger.info(message); } @bindThis public logMigration(message: string) { if (this.metricOnly) { return; } sqlLogger.info(message); } } export const entities = [ MiAnnouncement, MiAnnouncementRead, MiMeta, MiInstance, MiApp, MiAvatarDecoration, MiAuthSession, MiAccessToken, MiUser, MiUserProfile, MiUserKeypair, MiUserPublickey, MiUserList, MiUserListFavorite, MiUserListMembership, 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, MiAbuseReportNotificationRecipient, MiRegistrationTicket, MiSignin, MiModerationLog, MiClip, MiClipNote, MiClipFavorite, MiAntenna, MiPromoNote, MiPromoRead, MiRelay, MiChannel, MiChannelFollowing, MiChannelFavorite, MiRegistryItem, MiAd, MiPasswordResetRequest, MiUserPending, MiWebhook, MiSystemWebhook, MiUserIp, MiRetentionAggregation, MiRole, MiRoleAssignment, MiFlash, MiFlashLike, MiUserMemo, MiBubbleGameRecord, MiReversiGame, ...charts, ]; const log = process.env.NODE_ENV !== 'production'; export function createPostgresDataSource(config: Config, isMain = false) { 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, }, ...(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, })), }, } : {}), 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, family: config.redis.family ?? 0, password: config.redis.pass, keyPrefix: `${config.redis.prefix}:query:`, db: config.redis.db ?? 0, }, } : false, logging: true, logger: new MyCustomLogger(!log), maxQueryExecutionTime: 500, entities: entities, migrations: ['../../migration/*.js'], }); }