ログをデータベースに保存して管理画面で見れるように

This commit is contained in:
syuilo 2019-03-02 18:51:59 +09:00
parent 07f545a02e
commit 3d277fe4a0
29 changed files with 326 additions and 99 deletions

View file

@ -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: {

View file

@ -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>

View file

@ -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');

3
src/db/logger.ts Normal file
View file

@ -0,0 +1,3 @@
import Logger from '../services/logger';
export const dbLogger = new Logger('db');

View file

@ -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');

View file

@ -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);
}

View file

@ -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];

View file

@ -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);
}
}

View file

@ -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');

View file

@ -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');

View file

@ -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');

17
src/models/log.ts Normal file
View file

@ -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;
}

View file

@ -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 });

View file

@ -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');

View file

@ -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');

View file

@ -1,3 +1,3 @@
import Logger from '../misc/logger';
import Logger from '../services/logger';
export const queueLogger = new Logger('queue', 'orange');

View file

@ -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';

View file

@ -1,3 +1,3 @@
import Logger from "../misc/logger";
import Logger from "../services/logger";
export const remoteLogger = new Logger('remote', 'cyan');

View file

@ -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;
});

View file

@ -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');

View file

@ -1,3 +1,3 @@
import Logger from "../../misc/logger";
import Logger from "../../services/logger";
export const apiLogger = new Logger('api');

View file

@ -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();

View file

@ -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');

View file

@ -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');

View file

@ -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');

View file

@ -1,3 +1,3 @@
import Logger from "../../misc/logger";
import Logger from "../logger";
export const driveLogger = new Logger('drive', 'blue');

View file

@ -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';

View file

@ -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';

107
src/services/logger.ts Normal file
View file

@ -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);
}
}