yumechi-no-kuni/packages/backend/src/postgres.ts
eternal-flame-AD b78edccd19
Some checks failed
Publish Docker image / Build (push) Successful in 4m42s
Lint / pnpm_install (push) Successful in 1m27s
Test (production install and build) / production (22.11.0) (push) Successful in 1m6s
Test (backend) / unit (22.11.0) (push) Successful in 8m38s
Test (backend) / e2e (22.11.0) (push) Failing after 11m33s
Lint / lint (backend) (push) Successful in 2m38s
Lint / lint (frontend) (push) Successful in 2m32s
Lint / lint (frontend-embed) (push) Successful in 2m33s
Lint / lint (frontend-shared) (push) Successful in 2m30s
Lint / lint (misskey-bubble-game) (push) Successful in 2m29s
Lint / lint (misskey-js) (push) Successful in 2m28s
Lint / lint (misskey-reversi) (push) Successful in 2m25s
Lint / lint (sw) (push) Successful in 2m27s
Lint / typecheck (backend) (push) Successful in 2m26s
Lint / typecheck (misskey-js) (push) Successful in 1m40s
Lint / typecheck (sw) (push) Successful in 1m44s
Fix permetheus query fingerprinting (#24)
Signed-off-by: eternal-flame-AD <yume@yumechi.jp>

<!-- ℹ お読みください / README
PRありがとうございます! PRを作成する前に、コントリビューションガイドをご確認ください:
Thank you for your PR! Before creating a PR, please check the contribution guide:
https://github.com/misskey-dev/misskey/blob/develop/CONTRIBUTING.md
-->

## What
<!-- このPRで何をしたのか? どう変わるのか? -->
<!-- What did you do with this PR? How will it change things? -->

## Why
<!-- なぜそうするのか? どういう意図なのか? 何が困っているのか? -->
<!-- Why do you do it? What are your intentions? What is the problem? -->

## Additional info (optional)
<!-- テスト観点など -->
<!-- Test perspective, etc -->

## Checklist
- [ ] Read the [contribution guide](https://github.com/misskey-dev/misskey/blob/develop/CONTRIBUTING.md)
- [ ] Test working in a local environment
- [ ] (If needed) Add story of storybook
- [ ] (If needed) Update CHANGELOG.md
- [ ] (If possible) Add tests

Reviewed-on: #24
Co-authored-by: eternal-flame-AD <yume@yumechi.jp>
Co-committed-by: eternal-flame-AD <yume@yumechi.jp>
2024-11-18 03:29:53 -06:00

343 lines
10 KiB
TypeScript

/*
* 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<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)[\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<QueryTagCache>(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'],
});
}