From 977af0a24d3f0b3cb9799d133b02c6316f286660 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 2 Mar 2019 18:51:59 +0900
Subject: [PATCH] =?UTF-8?q?=E3=83=AD=E3=82=B0=E3=82=92=E3=83=87=E3=83=BC?=
 =?UTF-8?q?=E3=82=BF=E3=83=99=E3=83=BC=E3=82=B9=E3=81=AB=E4=BF=9D=E5=AD=98?=
 =?UTF-8?q?=E3=81=97=E3=81=A6=E7=AE=A1=E7=90=86=E7=94=BB=E9=9D=A2=E3=81=A7?=
 =?UTF-8?q?=E8=A6=8B=E3=82=8C=E3=82=8B=E3=82=88=E3=81=86=E3=81=AB?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/client/app/admin/views/index.vue       |   9 +-
 src/client/app/admin/views/logs.vue        | 101 +++++++++++++++++++
 src/db/elasticsearch.ts                    |   2 +-
 src/db/logger.ts                           |   3 +
 src/db/mongodb.ts                          |   3 -
 src/index.ts                               |  28 +++---
 src/misc/check-mongodb.ts                  |   2 +-
 src/misc/logger.ts                         |  59 ------------
 src/misc/show-machine-info.ts              |   2 +-
 src/models/drive-file.ts                   |   3 +-
 src/models/favorite.ts                     |   3 +-
 src/models/log.ts                          |  17 ++++
 src/models/note.ts                         |   3 +-
 src/models/notification.ts                 |   3 +-
 src/models/user.ts                         |   3 +-
 src/queue/logger.ts                        |   2 +-
 src/queue/processors/http/process-inbox.ts |   2 +-
 src/remote/logger.ts                       |   2 +-
 src/server/api/endpoints/admin/logs.ts     |  51 ++++++++++
 src/server/api/limiter.ts                  |   2 +-
 src/server/api/logger.ts                   |   2 +-
 src/server/index.ts                        |   4 +-
 src/server/web/url-preview.ts              |   2 +-
 src/services/blocking/delete.ts            |   2 +-
 src/services/chart/index.ts                |   2 +-
 src/services/drive/logger.ts               |   2 +-
 src/services/following/create.ts           |   2 +-
 src/services/following/delete.ts           |   2 +-
 src/services/logger.ts                     | 107 +++++++++++++++++++++
 29 files changed, 326 insertions(+), 99 deletions(-)
 create mode 100644 src/client/app/admin/views/logs.vue
 create mode 100644 src/db/logger.ts
 delete mode 100644 src/misc/logger.ts
 create mode 100644 src/models/log.ts
 create mode 100644 src/server/api/endpoints/admin/logs.ts
 create mode 100644 src/services/logger.ts

diff --git a/src/client/app/admin/views/index.vue b/src/client/app/admin/views/index.vue
index b37bb8f7f5..4bce197edb 100644
--- a/src/client/app/admin/views/index.vue
+++ b/src/client/app/admin/views/index.vue
@@ -21,6 +21,7 @@
 			<li @click="nav('dashboard')" :class="{ active: page == 'dashboard' }"><fa icon="home" fixed-width/>{{ $t('dashboard') }}</li>
 			<li @click="nav('instance')" :class="{ active: page == 'instance' }"><fa icon="cog" fixed-width/>{{ $t('instance') }}</li>
 			<li @click="nav('queue')" :class="{ active: page == 'queue' }"><fa :icon="faTasks" fixed-width/>{{ $t('queue') }}</li>
+			<li @click="nav('logs')" :class="{ active: page == 'logs' }"><fa :icon="faStream" fixed-width/>{{ $t('logs') }}</li>
 			<li @click="nav('moderators')" :class="{ active: page == 'moderators' }"><fa :icon="faHeadset" fixed-width/>{{ $t('moderators') }}</li>
 			<li @click="nav('users')" :class="{ active: page == 'users' }"><fa icon="users" fixed-width/>{{ $t('users') }}</li>
 			<li @click="nav('drive')" :class="{ active: page == 'drive' }"><fa icon="cloud" fixed-width/>{{ $t('@.drive') }}</li>
@@ -42,6 +43,7 @@
 			<div v-if="page == 'dashboard'"><x-dashboard/></div>
 			<div v-if="page == 'instance'"><x-instance/></div>
 			<div v-if="page == 'queue'"><x-queue/></div>
+			<div v-if="page == 'logs'"><x-logs/></div>
 			<div v-if="page == 'moderators'"><x-moderators/></div>
 			<div v-if="page == 'users'"><x-users/></div>
 			<div v-if="page == 'emoji'"><x-emoji/></div>
@@ -62,6 +64,7 @@ import { version } from '../../config';
 import XDashboard from "./dashboard.vue";
 import XInstance from "./instance.vue";
 import XQueue from "./queue.vue";
+import XLogs from "./logs.vue";
 import XModerators from "./moderators.vue";
 import XEmoji from "./emoji.vue";
 import XAnnouncements from "./announcements.vue";
@@ -71,7 +74,7 @@ import XDrive from "./drive.vue";
 import XAbuse from "./abuse.vue";
 import XFederation from "./federation.vue";
 
-import { faHeadset, faArrowLeft, faGlobe, faExclamationCircle, faTasks } from '@fortawesome/free-solid-svg-icons';
+import { faHeadset, faArrowLeft, faGlobe, faExclamationCircle, faTasks, faStream } from '@fortawesome/free-solid-svg-icons';
 import { faGrin } from '@fortawesome/free-regular-svg-icons';
 
 // Detect the user agent
@@ -84,6 +87,7 @@ export default Vue.extend({
 		XDashboard,
 		XInstance,
 		XQueue,
+		XLogs,
 		XModerators,
 		XEmoji,
 		XAnnouncements,
@@ -107,7 +111,8 @@ export default Vue.extend({
 			faHeadset,
 			faGlobe,
 			faExclamationCircle,
-			faTasks
+			faTasks,
+			faStream
 		};
 	},
 	methods: {
diff --git a/src/client/app/admin/views/logs.vue b/src/client/app/admin/views/logs.vue
new file mode 100644
index 0000000000..1e76d141b4
--- /dev/null
+++ b/src/client/app/admin/views/logs.vue
@@ -0,0 +1,101 @@
+<template>
+<div>
+	<ui-card>
+		<template #title><fa :icon="faStream"/> {{ $t('logs') }}</template>
+		<section class="fit-top">
+			<ui-horizon-group inputs>
+				<ui-input v-model="domain">
+					<span>{{ $t('domain') }}</span>
+				</ui-input>
+				<ui-select v-model="level">
+					<template #label>{{ $t('level') }}</template>
+					<option value="all">{{ $t('levels.all') }}</option>
+					<option value="info">{{ $t('levels.info') }}</option>
+					<option value="success">{{ $t('levels.success') }}</option>
+					<option value="warning">{{ $t('levels.warning') }}</option>
+					<option value="error">{{ $t('levels.error') }}</option>
+					<option value="debug">{{ $t('levels.debug') }}</option>
+				</ui-select>
+			</ui-horizon-group>
+
+			<div class="nqjzuvev">
+				<code v-for="log in logs" :key="log._id" :class="log.level">
+					<mk-time :time="log.createdAt"/> [{{ log.domain.join(' ') }}] {{ log.message }}
+				</code>
+			</div>
+		</section>
+	</ui-card>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import i18n from '../../i18n';
+import { faStream } from '@fortawesome/free-solid-svg-icons';
+
+export default Vue.extend({
+	i18n: i18n('admin/views/logs.vue'),
+
+	data() {
+		return {
+			logs: [],
+			level: 'all',
+			domain: '',
+			faStream
+		};
+	},
+
+	watch: {
+		level() {
+			this.logs = [];
+			this.fetch();
+		},
+
+		domain() {
+			this.logs = [];
+			this.fetch();
+		}
+	},
+
+	mounted() {
+		this.fetch();
+	},
+
+	methods: {
+		fetch() {
+			this.$root.api('admin/logs', {
+				level: this.level === 'all' ? null : this.level,
+				domain: this.domain === '' ? null : this.domain,
+				limit: 50
+			}).then(logs => {
+				this.logs = logs;
+			});
+		}
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.nqjzuvev
+	white-space nowrap
+	overflow auto
+	padding 8px
+	background #000
+	color #fff
+
+	> code
+		display block
+
+		&.error
+			color #f00
+
+		&.warning
+			color #ff0
+
+		&.success
+			color #0f0
+
+		&.debug
+			opacity 0.7
+
+</style>
diff --git a/src/db/elasticsearch.ts b/src/db/elasticsearch.ts
index cbe6afbbb9..d54b01763b 100644
--- a/src/db/elasticsearch.ts
+++ b/src/db/elasticsearch.ts
@@ -1,6 +1,6 @@
 import * as elasticsearch from 'elasticsearch';
 import config from '../config';
-import Logger from '../misc/logger';
+import Logger from '../services/logger';
 
 const esLogger = new Logger('es');
 
diff --git a/src/db/logger.ts b/src/db/logger.ts
new file mode 100644
index 0000000000..1f702c18e2
--- /dev/null
+++ b/src/db/logger.ts
@@ -0,0 +1,3 @@
+import Logger from '../services/logger';
+
+export const dbLogger = new Logger('db');
diff --git a/src/db/mongodb.ts b/src/db/mongodb.ts
index dedb289ce9..f82ced1765 100644
--- a/src/db/mongodb.ts
+++ b/src/db/mongodb.ts
@@ -18,7 +18,6 @@ export default db;
  * MongoDB native module (officialy)
  */
 import * as mongodb from 'mongodb';
-import Logger from '../misc/logger';
 
 let mdb: mongodb.Db;
 
@@ -38,5 +37,3 @@ const nativeDbConn = async (): Promise<mongodb.Db> => {
 };
 
 export { nativeDbConn };
-
-export const dbLogger = new Logger('db');
diff --git a/src/index.ts b/src/index.ts
index 6983ec722e..3206ee3d28 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -13,7 +13,7 @@ import * as portscanner from 'portscanner';
 import * as isRoot from 'is-root';
 import Xev from 'xev';
 
-import Logger from './misc/logger';
+import Logger from './services/logger';
 import serverStats from './daemons/server-stats';
 import notesStats from './daemons/notes-stats';
 import loadConfig from './config/load';
@@ -25,7 +25,7 @@ import { checkMongoDB } from './misc/check-mongodb';
 import { showMachineInfo } from './misc/show-machine-info';
 
 const logger = new Logger('core', 'cyan');
-const bootLogger = logger.createSubLogger('boot', 'magenta');
+const bootLogger = logger.createSubLogger('boot', 'magenta', false);
 const clusterLogger = logger.createSubLogger('cluster', 'orange');
 const ev = new Xev();
 
@@ -73,7 +73,7 @@ function greet() {
 	console.log(chalk`${os.hostname()} {gray (PID: ${process.pid.toString()})}`);
 
 	bootLogger.info('Welcome to Misskey!');
-	bootLogger.info(`Misskey v${pkg.version}`, true);
+	bootLogger.info(`Misskey v${pkg.version}`, null, true);
 	bootLogger.info('Misskey is maintained by @syuilo, @AyaMorisawa, @mei23, and @acid-chicken.');
 }
 
@@ -90,21 +90,21 @@ async function masterMain() {
 		config = await init();
 
 		if (config.port == null) {
-			bootLogger.error('The port is not configured. Please configure port.', true);
+			bootLogger.error('The port is not configured. Please configure port.', null, true);
 			process.exit(1);
 		}
 
 		if (process.platform === 'linux' && isWellKnownPort(config.port) && !isRoot()) {
-			bootLogger.error('You need root privileges to listen on well-known port on Linux', true);
+			bootLogger.error('You need root privileges to listen on well-known port on Linux', null, true);
 			process.exit(1);
 		}
 
 		if (!await isPortAvailable(config.port)) {
-			bootLogger.error(`Port ${config.port} is already in use`, true);
+			bootLogger.error(`Port ${config.port} is already in use`, null, true);
 			process.exit(1);
 		}
 	} catch (e) {
-		bootLogger.error('Fatal error occurred during initialization', true);
+		bootLogger.error('Fatal error occurred during initialization', null, true);
 		process.exit(1);
 	}
 
@@ -117,7 +117,7 @@ async function masterMain() {
 	// start queue
 	require('./queue').default();
 
-	bootLogger.succ(`Now listening on port ${config.port} on ${config.url}`, true);
+	bootLogger.succ(`Now listening on port ${config.port} on ${config.url}`, null, true);
 }
 
 /**
@@ -140,7 +140,7 @@ async function queueMain() {
 		// initialize app
 		await init();
 	} catch (e) {
-		bootLogger.error('Fatal error occurred during initialization', true);
+		bootLogger.error('Fatal error occurred during initialization', null, true);
 		process.exit(1);
 	}
 
@@ -150,7 +150,7 @@ async function queueMain() {
 	const queue = require('./queue').default();
 
 	if (queue) {
-		bootLogger.succ('Queue started', true);
+		bootLogger.succ('Queue started', null, true);
 	} else {
 		bootLogger.error('Queue not available');
 	}
@@ -175,7 +175,7 @@ function showEnvironment(): void {
 
 	if (env !== 'production') {
 		logger.warn('The environment is not in production mode.');
-		logger.warn('DO NOT USE FOR PRODUCTION PURPOSE!', true);
+		logger.warn('DO NOT USE FOR PRODUCTION PURPOSE!', null, true);
 	}
 
 	logger.info(`You ${isRoot() ? '' : 'do not '}have root privileges`);
@@ -192,7 +192,7 @@ async function init(): Promise<Config> {
 	nodejsLogger.info(`Version ${runningNodejsVersion.join('.')}`);
 
 	if (!satisfyNodejsVersion) {
-		nodejsLogger.error(`Node.js version is less than ${requiredNodejsVersion.join('.')}. Please upgrade it.`, true);
+		nodejsLogger.error(`Node.js version is less than ${requiredNodejsVersion.join('.')}. Please upgrade it.`, null, true);
 		process.exit(1);
 	}
 
@@ -209,7 +209,7 @@ async function init(): Promise<Config> {
 			process.exit(1);
 		}
 		if (exception.code === 'ENOENT') {
-			configLogger.error('Configuration file not found', true);
+			configLogger.error('Configuration file not found', null, true);
 			process.exit(1);
 		}
 		throw exception;
@@ -221,7 +221,7 @@ async function init(): Promise<Config> {
 	try {
 		await checkMongoDB(config, bootLogger);
 	} catch (e) {
-		bootLogger.error('Cannot connect to database', true);
+		bootLogger.error('Cannot connect to database', null, true);
 		process.exit(1);
 	}
 
diff --git a/src/misc/check-mongodb.ts b/src/misc/check-mongodb.ts
index f3839da31f..8e03db5d42 100644
--- a/src/misc/check-mongodb.ts
+++ b/src/misc/check-mongodb.ts
@@ -1,6 +1,6 @@
 import { nativeDbConn } from '../db/mongodb';
 import { Config } from '../config/types';
-import Logger from './logger';
+import Logger from '../services/logger';
 import { lessThan } from '../prelude/array';
 
 const requiredMongoDBVersion = [3, 6];
diff --git a/src/misc/logger.ts b/src/misc/logger.ts
deleted file mode 100644
index 1d159fb6a5..0000000000
--- a/src/misc/logger.ts
+++ /dev/null
@@ -1,59 +0,0 @@
-import * as cluster from 'cluster';
-import chalk from 'chalk';
-import * as dateformat from 'dateformat';
-import { program } from '../argv';
-
-export default class Logger {
-	private domain: string;
-	private color?: string;
-	private parentLogger: Logger;
-
-	constructor(domain: string, color?: string) {
-		this.domain = domain;
-		this.color = color;
-	}
-
-	public createSubLogger(domain: string, color?: string): Logger {
-		const logger = new Logger(domain, color);
-		logger.parentLogger = this;
-		return logger;
-	}
-
-	private log(level: string, message: string, important = false, subDomains: string[] = []): void {
-		if (program.quiet) return;
-		if (process.env.NODE_ENV === 'test') return;
-		const domain = this.color ? chalk.keyword(this.color)(this.domain) : chalk.white(this.domain);
-		const domains = [domain].concat(subDomains);
-		if (this.parentLogger) {
-			this.parentLogger.log(level, message, important, domains);
-		} else {
-			const time = dateformat(new Date(), 'HH:MM:ss');
-			const process = cluster.isMaster ? '*' : cluster.worker.id;
-			let log = `${level} ${process}\t[${domains.join(' ')}]\t${message}`;
-			if (program.withLogTime) log = chalk.gray(time) + ' ' + log;
-			console.log(important ? chalk.bold(log) : log);
-		}
-	}
-
-	public error(message: string | Error, important = false): void { // 実行を継続できない状況で使う
-		this.log(important ? chalk.bgRed.white('ERR ') : chalk.red('ERR '), chalk.red(message.toString()), important);
-	}
-
-	public warn(message: string, important = false): void { // 実行を継続できるが改善すべき状況で使う
-		this.log(chalk.yellow('WARN'), chalk.yellow(message), important);
-	}
-
-	public succ(message: string, important = false): void { // 何かに成功した状況で使う
-		this.log(important ? chalk.bgGreen.white('DONE') : chalk.green('DONE'), chalk.green(message), important);
-	}
-
-	public debug(message: string, important = false): void { // デバッグ用に使う(開発者に必要だが利用者に不要な情報)
-		if (process.env.NODE_ENV != 'production' || program.verbose) {
-			this.log(chalk.gray('VERB'), chalk.gray(message), important);
-		}
-	}
-
-	public info(message: string, important = false): void { // それ以外
-		this.log(chalk.blue('INFO'), message, important);
-	}
-}
diff --git a/src/misc/show-machine-info.ts b/src/misc/show-machine-info.ts
index a4d85edfe0..2aae019be2 100644
--- a/src/misc/show-machine-info.ts
+++ b/src/misc/show-machine-info.ts
@@ -1,6 +1,6 @@
 import * as os from 'os';
 import * as sysUtils from 'systeminformation';
-import Logger from './logger';
+import Logger from '../services/logger';
 
 export async function showMachineInfo(parentLogger: Logger) {
 	const logger = parentLogger.createSubLogger('machine');
diff --git a/src/models/drive-file.ts b/src/models/drive-file.ts
index f3e21f209b..c31e9a709f 100644
--- a/src/models/drive-file.ts
+++ b/src/models/drive-file.ts
@@ -2,9 +2,10 @@ import * as mongo from 'mongodb';
 import * as deepcopy from 'deepcopy';
 import { pack as packFolder } from './drive-folder';
 import { pack as packUser } from './user';
-import monkDb, { nativeDbConn, dbLogger } from '../db/mongodb';
+import monkDb, { nativeDbConn } from '../db/mongodb';
 import isObjectId from '../misc/is-objectid';
 import getDriveFileUrl, { getOriginalUrl } from '../misc/get-drive-file-url';
+import { dbLogger } from '../db/logger';
 
 const DriveFile = monkDb.get<IDriveFile>('driveFiles.files');
 DriveFile.createIndex('md5');
diff --git a/src/models/favorite.ts b/src/models/favorite.ts
index e3aa92c887..2008edbfaf 100644
--- a/src/models/favorite.ts
+++ b/src/models/favorite.ts
@@ -1,8 +1,9 @@
 import * as mongo from 'mongodb';
 import * as deepcopy from 'deepcopy';
-import db, { dbLogger } from '../db/mongodb';
+import db from '../db/mongodb';
 import isObjectId from '../misc/is-objectid';
 import { pack as packNote } from './note';
+import { dbLogger } from '../db/logger';
 
 const Favorite = db.get<IFavorite>('favorites');
 Favorite.createIndex('userId');
diff --git a/src/models/log.ts b/src/models/log.ts
new file mode 100644
index 0000000000..f74332e940
--- /dev/null
+++ b/src/models/log.ts
@@ -0,0 +1,17 @@
+import * as mongo from 'mongodb';
+import db from '../db/mongodb';
+
+const Log = db.get<ILog>('logs');
+Log.createIndex('createdAt', { expireAfterSeconds: 3600 * 24 * 3 });
+export default Log;
+
+export interface ILog {
+	_id: mongo.ObjectID;
+	createdAt: Date;
+	machine: string;
+	worker: string;
+	domain: string[];
+	level: string;
+	message: string;
+	data: any;
+}
diff --git a/src/models/note.ts b/src/models/note.ts
index 9bdf22e977..c3413164be 100644
--- a/src/models/note.ts
+++ b/src/models/note.ts
@@ -1,7 +1,7 @@
 import * as mongo from 'mongodb';
 import * as deepcopy from 'deepcopy';
 import rap from '@prezzemolo/rap';
-import db, { dbLogger } from '../db/mongodb';
+import db from '../db/mongodb';
 import isObjectId from '../misc/is-objectid';
 import { length } from 'stringz';
 import { IUser, pack as packUser } from './user';
@@ -11,6 +11,7 @@ import Reaction from './note-reaction';
 import { packMany as packFileMany, IDriveFile } from './drive-file';
 import Following from './following';
 import Emoji from './emoji';
+import { dbLogger } from '../db/logger';
 
 const Note = db.get<INote>('notes');
 Note.createIndex('uri', { sparse: true, unique: true });
diff --git a/src/models/notification.ts b/src/models/notification.ts
index aedeafb522..75456af57b 100644
--- a/src/models/notification.ts
+++ b/src/models/notification.ts
@@ -1,9 +1,10 @@
 import * as mongo from 'mongodb';
 import * as deepcopy from 'deepcopy';
-import db, { dbLogger } from '../db/mongodb';
+import db from '../db/mongodb';
 import isObjectId from '../misc/is-objectid';
 import { IUser, pack as packUser } from './user';
 import { pack as packNote } from './note';
+import { dbLogger } from '../db/logger';
 
 const Notification = db.get<INotification>('notifications');
 Notification.createIndex('notifieeId');
diff --git a/src/models/user.ts b/src/models/user.ts
index 97c7037938..56e052ed46 100644
--- a/src/models/user.ts
+++ b/src/models/user.ts
@@ -1,7 +1,7 @@
 import * as mongo from 'mongodb';
 import * as deepcopy from 'deepcopy';
 import rap from '@prezzemolo/rap';
-import db, { dbLogger } from '../db/mongodb';
+import db from '../db/mongodb';
 import isObjectId from '../misc/is-objectid';
 import { packMany as packNoteMany } from './note';
 import Following from './following';
@@ -12,6 +12,7 @@ import config from '../config';
 import FollowRequest from './follow-request';
 import fetchMeta from '../misc/fetch-meta';
 import Emoji from './emoji';
+import { dbLogger } from '../db/logger';
 
 const User = db.get<IUser>('users');
 
diff --git a/src/queue/logger.ts b/src/queue/logger.ts
index 99d88bd63b..d6d0774680 100644
--- a/src/queue/logger.ts
+++ b/src/queue/logger.ts
@@ -1,3 +1,3 @@
-import Logger from '../misc/logger';
+import Logger from '../services/logger';
 
 export const queueLogger = new Logger('queue', 'orange');
diff --git a/src/queue/processors/http/process-inbox.ts b/src/queue/processors/http/process-inbox.ts
index 43170848f9..ea737593dc 100644
--- a/src/queue/processors/http/process-inbox.ts
+++ b/src/queue/processors/http/process-inbox.ts
@@ -7,7 +7,7 @@ import { resolvePerson, updatePerson } from '../../../remote/activitypub/models/
 import { toUnicode } from 'punycode';
 import { URL } from 'url';
 import { publishApLogStream } from '../../../services/stream';
-import Logger from '../../../misc/logger';
+import Logger from '../../../services/logger';
 import { registerOrFetchInstanceDoc } from '../../../services/register-or-fetch-instance-doc';
 import Instance from '../../../models/instance';
 import instanceChart from '../../../services/chart/instance';
diff --git a/src/remote/logger.ts b/src/remote/logger.ts
index cf2fe1e909..43b4f105aa 100644
--- a/src/remote/logger.ts
+++ b/src/remote/logger.ts
@@ -1,3 +1,3 @@
-import Logger from "../misc/logger";
+import Logger from "../services/logger";
 
 export const remoteLogger = new Logger('remote', 'cyan');
diff --git a/src/server/api/endpoints/admin/logs.ts b/src/server/api/endpoints/admin/logs.ts
new file mode 100644
index 0000000000..b5dc7d7283
--- /dev/null
+++ b/src/server/api/endpoints/admin/logs.ts
@@ -0,0 +1,51 @@
+import $ from 'cafy';
+import define from '../../define';
+import Log from '../../../../models/log';
+
+export const meta = {
+	tags: ['admin'],
+
+	requireCredential: true,
+	requireModerator: true,
+
+	params: {
+		limit: {
+			validator: $.optional.num.range(1, 100),
+			default: 30
+		},
+
+		level: {
+			validator: $.optional.nullable.str,
+			default: null as any
+		},
+
+		domain: {
+			validator: $.optional.nullable.str,
+			default: null as any
+		}
+	}
+};
+
+export default define(meta, async (ps) => {
+	const sort = {
+		_id: -1
+	};
+	const query = {} as any;
+
+	if (ps.level) query.level = ps.level;
+	if (ps.domain) {
+		let i = 0;
+		for (const d of ps.domain.split(' ')) {
+			query[`domain.${i}`] = d;
+			i++;
+		}
+	}
+
+	const logs = await Log
+		.find(query, {
+			limit: ps.limit,
+			sort: sort
+		});
+
+	return logs;
+});
diff --git a/src/server/api/limiter.ts b/src/server/api/limiter.ts
index f5c326f7d1..3d66172fd8 100644
--- a/src/server/api/limiter.ts
+++ b/src/server/api/limiter.ts
@@ -3,7 +3,7 @@ import limiterDB from '../../db/redis';
 import { IEndpoint } from './endpoints';
 import getAcct from '../../misc/acct/render';
 import { IUser } from '../../models/user';
-import Logger from '../../misc/logger';
+import Logger from '../../services/logger';
 
 const logger = new Logger('limiter');
 
diff --git a/src/server/api/logger.ts b/src/server/api/logger.ts
index 334a696331..0ea67a90eb 100644
--- a/src/server/api/logger.ts
+++ b/src/server/api/logger.ts
@@ -1,3 +1,3 @@
-import Logger from "../../misc/logger";
+import Logger from "../../services/logger";
 
 export const apiLogger = new Logger('api');
diff --git a/src/server/index.ts b/src/server/index.ts
index 470562d802..7c51923f9e 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -23,10 +23,10 @@ import networkChart from '../services/chart/network';
 import apiServer from './api';
 import { sum } from '../prelude/array';
 import User from '../models/user';
-import Logger from '../misc/logger';
+import Logger from '../services/logger';
 import { program } from '../argv';
 
-export const serverLogger = new Logger('server', 'gray');
+export const serverLogger = new Logger('server', 'gray', false);
 
 // Init app
 const app = new Koa();
diff --git a/src/server/web/url-preview.ts b/src/server/web/url-preview.ts
index 9fda5f16f8..90c1a4930a 100644
--- a/src/server/web/url-preview.ts
+++ b/src/server/web/url-preview.ts
@@ -2,7 +2,7 @@ import * as Koa from 'koa';
 import * as request from 'request-promise-native';
 import summaly from 'summaly';
 import fetchMeta from '../../misc/fetch-meta';
-import Logger from '../../misc/logger';
+import Logger from '../../services/logger';
 
 const logger = new Logger('url-preview');
 
diff --git a/src/services/blocking/delete.ts b/src/services/blocking/delete.ts
index 425648f4cc..099fa14b37 100644
--- a/src/services/blocking/delete.ts
+++ b/src/services/blocking/delete.ts
@@ -4,7 +4,7 @@ import { renderActivity } from '../../remote/activitypub/renderer';
 import renderBlock from '../../remote/activitypub/renderer/block';
 import renderUndo from '../../remote/activitypub/renderer/undo';
 import { deliver } from '../../queue';
-import Logger from '../../misc/logger';
+import Logger from '../logger';
 
 const logger = new Logger('blocking/delete');
 
diff --git a/src/services/chart/index.ts b/src/services/chart/index.ts
index 2fabb55721..7a6470f4d8 100644
--- a/src/services/chart/index.ts
+++ b/src/services/chart/index.ts
@@ -8,7 +8,7 @@ import autobind from 'autobind-decorator';
 import * as mongo from 'mongodb';
 import db from '../../db/mongodb';
 import { ICollection } from 'monk';
-import Logger from '../../misc/logger';
+import Logger from '../logger';
 import { Schema } from '../../misc/schema';
 
 const logger = new Logger('chart');
diff --git a/src/services/drive/logger.ts b/src/services/drive/logger.ts
index 979d282cc1..b66db9ed8f 100644
--- a/src/services/drive/logger.ts
+++ b/src/services/drive/logger.ts
@@ -1,3 +1,3 @@
-import Logger from "../../misc/logger";
+import Logger from "../logger";
 
 export const driveLogger = new Logger('drive', 'blue');
diff --git a/src/services/following/create.ts b/src/services/following/create.ts
index 38f86fbf7d..1eaad750f7 100644
--- a/src/services/following/create.ts
+++ b/src/services/following/create.ts
@@ -13,7 +13,7 @@ import perUserFollowingChart from '../../services/chart/per-user-following';
 import { registerOrFetchInstanceDoc } from '../register-or-fetch-instance-doc';
 import Instance from '../../models/instance';
 import instanceChart from '../../services/chart/instance';
-import Logger from '../../misc/logger';
+import Logger from '../logger';
 import FollowRequest from '../../models/follow-request';
 import { IdentifiableError } from '../../misc/identifiable-error';
 
diff --git a/src/services/following/delete.ts b/src/services/following/delete.ts
index 93f72b51d8..28268f1fd8 100644
--- a/src/services/following/delete.ts
+++ b/src/services/following/delete.ts
@@ -6,7 +6,7 @@ import renderFollow from '../../remote/activitypub/renderer/follow';
 import renderUndo from '../../remote/activitypub/renderer/undo';
 import { deliver } from '../../queue';
 import perUserFollowingChart from '../../services/chart/per-user-following';
-import Logger from '../../misc/logger';
+import Logger from '../logger';
 import { registerOrFetchInstanceDoc } from '../register-or-fetch-instance-doc';
 import Instance from '../../models/instance';
 import instanceChart from '../../services/chart/instance';
diff --git a/src/services/logger.ts b/src/services/logger.ts
new file mode 100644
index 0000000000..aa93954bc1
--- /dev/null
+++ b/src/services/logger.ts
@@ -0,0 +1,107 @@
+import * as cluster from 'cluster';
+import * as os from 'os';
+import chalk from 'chalk';
+import * as dateformat from 'dateformat';
+import { program } from '../argv';
+import Log from '../models/log';
+
+type Domain = {
+	name: string;
+	color: string;
+};
+
+type Level = 'error' | 'success' | 'warning' | 'debug' | 'info';
+
+export default class Logger {
+	private domain: Domain;
+	private parentLogger: Logger;
+	private store: boolean;
+
+	constructor(domain: string, color?: string, store = true) {
+		this.domain = {
+			name: domain,
+			color: color,
+		};
+		this.store = store;
+	}
+
+	public createSubLogger(domain: string, color?: string, store = true): Logger {
+		const logger = new Logger(domain, color, store);
+		logger.parentLogger = this;
+		return logger;
+	}
+
+	private log(level: Level, message: string, data: Record<string, any>, important = false, subDomains: Domain[] = [], store = true): void {
+		if (program.quiet) return;
+		if (process.env.NODE_ENV === 'test') return;
+		if (!this.store) store = false;
+
+		if (this.parentLogger) {
+			this.parentLogger.log(level, message, data, important, [this.domain].concat(subDomains), store);
+			return;
+		}
+
+		const time = dateformat(new Date(), 'HH:MM:ss');
+		const worker = cluster.isMaster ? '*' : cluster.worker.id;
+		const l =
+			level === 'error' ? important ? chalk.bgRed.white('ERR ') : chalk.red('ERR ') :
+			level === 'warning' ? chalk.yellow('WARN') :
+			level === 'success' ? important ? chalk.bgGreen.white('DONE') : chalk.green('DONE') :
+			level === 'debug' ? chalk.gray('VERB') :
+			level === 'info' ? chalk.blue('INFO') :
+			null;
+		const domains = [this.domain].concat(subDomains).map(d => d.color ? chalk.keyword(d.color)(d.name) : chalk.white(d.name));
+		const m =
+			level === 'error' ? chalk.red(message) :
+			level === 'warning' ? chalk.yellow(message) :
+			level === 'success' ? chalk.green(message) :
+			level === 'debug' ? chalk.gray(message) :
+			level === 'info' ? message :
+			null;
+
+		let log = `${l} ${worker}\t[${domains.join(' ')}]\t${m}`;
+		if (program.withLogTime) log = chalk.gray(time) + ' ' + log;
+
+		console.log(important ? chalk.bold(log) : log);
+
+		if (store) {
+			Log.insert({
+				createdAt: new Date(),
+				machine: os.hostname(),
+				worker: worker,
+				domain: [this.domain].concat(subDomains).map(d => d.name),
+				level: level,
+				message: message,
+				data: data,
+			});
+		}
+	}
+
+	public error(x: string | Error, data?: Record<string, any>, important = false): void { // 実行を継続できない状況で使う
+		if (x instanceof Error) {
+			data = data || {};
+			data.e = x;
+			this.log('error', x.toString(), data, important);
+		} else {
+			this.log('error', x, data, important);
+		}
+	}
+
+	public warn(message: string, data?: Record<string, any>, important = false): void { // 実行を継続できるが改善すべき状況で使う
+		this.log('warning', message, data, important);
+	}
+
+	public succ(message: string, data?: Record<string, any>, important = false): void { // 何かに成功した状況で使う
+		this.log('success', message, data, important);
+	}
+
+	public debug(message: string, data?: Record<string, any>, important = false): void { // デバッグ用に使う(開発者に必要だが利用者に不要な情報)
+		if (process.env.NODE_ENV != 'production' || program.verbose) {
+			this.log('debug', message, data, important);
+		}
+	}
+
+	public info(message: string, data?: Record<string, any>, important = false): void { // それ以外
+		this.log('info', message, data, important);
+	}
+}