diff options
24 files changed, 435 insertions, 77 deletions
diff --git a/client/src/app/+admin/jobs/job.component.ts b/client/src/app/+admin/jobs/job.component.ts deleted file mode 100644 index bc80c9a6a..000000000 --- a/client/src/app/+admin/jobs/job.component.ts +++ /dev/null | |||
@@ -1,6 +0,0 @@ | |||
1 | import { Component } from '@angular/core' | ||
2 | |||
3 | @Component({ | ||
4 | template: '<router-outlet></router-outlet>' | ||
5 | }) | ||
6 | export class JobsComponent {} | ||
diff --git a/client/src/app/+admin/jobs/job.routes.ts b/client/src/app/+admin/jobs/job.routes.ts deleted file mode 100644 index 331dc2af2..000000000 --- a/client/src/app/+admin/jobs/job.routes.ts +++ /dev/null | |||
@@ -1,32 +0,0 @@ | |||
1 | import { Routes } from '@angular/router' | ||
2 | import { UserRight } from '../../../../../shared' | ||
3 | import { UserRightGuard } from '../../core' | ||
4 | import { JobsComponent } from './job.component' | ||
5 | import { JobsListComponent } from './jobs-list/jobs-list.component' | ||
6 | |||
7 | export const JobsRoutes: Routes = [ | ||
8 | { | ||
9 | path: 'jobs', | ||
10 | component: JobsComponent, | ||
11 | canActivate: [ UserRightGuard ], | ||
12 | data: { | ||
13 | userRight: UserRight.MANAGE_JOBS | ||
14 | }, | ||
15 | children: [ | ||
16 | { | ||
17 | path: '', | ||
18 | redirectTo: 'list', | ||
19 | pathMatch: 'full' | ||
20 | }, | ||
21 | { | ||
22 | path: 'list', | ||
23 | component: JobsListComponent, | ||
24 | data: { | ||
25 | meta: { | ||
26 | title: 'Jobs list' | ||
27 | } | ||
28 | } | ||
29 | } | ||
30 | ] | ||
31 | } | ||
32 | ] | ||
diff --git a/client/src/app/+admin/jobs/jobs-list/index.ts b/client/src/app/+admin/jobs/jobs-list/index.ts deleted file mode 100644 index cf590a6f8..000000000 --- a/client/src/app/+admin/jobs/jobs-list/index.ts +++ /dev/null | |||
@@ -1 +0,0 @@ | |||
1 | export * from './jobs-list.component' | ||
diff --git a/client/src/app/+admin/jobs/shared/index.ts b/client/src/app/+admin/jobs/shared/index.ts deleted file mode 100644 index 609439e5c..000000000 --- a/client/src/app/+admin/jobs/shared/index.ts +++ /dev/null | |||
@@ -1 +0,0 @@ | |||
1 | export * from './job.service' | ||
diff --git a/client/src/app/+admin/jobs/index.ts b/client/src/app/+admin/system/jobs/index.ts index c0e0cc95d..c0e0cc95d 100644 --- a/client/src/app/+admin/jobs/index.ts +++ b/client/src/app/+admin/system/jobs/index.ts | |||
diff --git a/client/src/app/+admin/jobs/shared/job.service.ts b/client/src/app/+admin/system/jobs/job.service.ts index b96dc3359..b96dc3359 100644 --- a/client/src/app/+admin/jobs/shared/job.service.ts +++ b/client/src/app/+admin/system/jobs/job.service.ts | |||
diff --git a/client/src/app/+admin/jobs/jobs-list/jobs-list.component.html b/client/src/app/+admin/system/jobs/jobs.component.html index 7ed1888e2..7ed1888e2 100644 --- a/client/src/app/+admin/jobs/jobs-list/jobs-list.component.html +++ b/client/src/app/+admin/system/jobs/jobs.component.html | |||
diff --git a/client/src/app/+admin/jobs/jobs-list/jobs-list.component.scss b/client/src/app/+admin/system/jobs/jobs.component.scss index ab05f1982..ab05f1982 100644 --- a/client/src/app/+admin/jobs/jobs-list/jobs-list.component.scss +++ b/client/src/app/+admin/system/jobs/jobs.component.scss | |||
diff --git a/client/src/app/+admin/jobs/jobs-list/jobs-list.component.ts b/client/src/app/+admin/system/jobs/jobs.component.ts index b265e1dd6..b265e1dd6 100644 --- a/client/src/app/+admin/jobs/jobs-list/jobs-list.component.ts +++ b/client/src/app/+admin/system/jobs/jobs.component.ts | |||
diff --git a/scripts/parse-log.ts b/scripts/parse-log.ts index 86aaa7994..66a5b8719 100755 --- a/scripts/parse-log.ts +++ b/scripts/parse-log.ts | |||
@@ -1,10 +1,11 @@ | |||
1 | import * as program from 'commander' | 1 | import * as program from 'commander' |
2 | import { createReadStream, readdirSync, statSync } from 'fs-extra' | 2 | import { createReadStream, readdir } from 'fs-extra' |
3 | import { join } from 'path' | 3 | import { join } from 'path' |
4 | import { createInterface } from 'readline' | 4 | import { createInterface } from 'readline' |
5 | import * as winston from 'winston' | 5 | import * as winston from 'winston' |
6 | import { labelFormatter } from '../server/helpers/logger' | 6 | import { labelFormatter } from '../server/helpers/logger' |
7 | import { CONFIG } from '../server/initializers/constants' | 7 | import { CONFIG } from '../server/initializers/constants' |
8 | import { mtimeSortFilesDesc } from '../shared/utils/logs/logs' | ||
8 | 9 | ||
9 | program | 10 | program |
10 | .option('-l, --level [level]', 'Level log (debug/info/warn/error)') | 11 | .option('-l, --level [level]', 'Level log (debug/info/warn/error)') |
@@ -52,42 +53,47 @@ const logLevels = { | |||
52 | debug: logger.debug.bind(logger) | 53 | debug: logger.debug.bind(logger) |
53 | } | 54 | } |
54 | 55 | ||
55 | const logFiles = readdirSync(CONFIG.STORAGE.LOG_DIR) | 56 | run() |
56 | const lastLogFile = getNewestFile(logFiles, CONFIG.STORAGE.LOG_DIR) | 57 | .then(() => process.exit(0)) |
58 | .catch(err => console.error(err)) | ||
57 | 59 | ||
58 | const path = join(CONFIG.STORAGE.LOG_DIR, lastLogFile) | 60 | function run () { |
59 | console.log('Opening %s.', path) | 61 | return new Promise(async res => { |
62 | const logFiles = await readdir(CONFIG.STORAGE.LOG_DIR) | ||
63 | const lastLogFile = await getNewestFile(logFiles, CONFIG.STORAGE.LOG_DIR) | ||
60 | 64 | ||
61 | const rl = createInterface({ | 65 | const path = join(CONFIG.STORAGE.LOG_DIR, lastLogFile) |
62 | input: createReadStream(path) | 66 | console.log('Opening %s.', path) |
63 | }) | ||
64 | 67 | ||
65 | rl.on('line', line => { | 68 | const stream = createReadStream(path) |
66 | const log = JSON.parse(line) | ||
67 | // Don't know why but loggerFormat does not remove splat key | ||
68 | Object.assign(log, { splat: undefined }) | ||
69 | 69 | ||
70 | logLevels[log.level](log) | 70 | const rl = createInterface({ |
71 | }) | 71 | input: stream |
72 | }) | ||
72 | 73 | ||
73 | function toTimeFormat (time: string) { | 74 | rl.on('line', line => { |
74 | const timestamp = Date.parse(time) | 75 | const log = JSON.parse(line) |
76 | // Don't know why but loggerFormat does not remove splat key | ||
77 | Object.assign(log, { splat: undefined }) | ||
75 | 78 | ||
76 | if (isNaN(timestamp) === true) return 'Unknown date' | 79 | logLevels[ log.level ](log) |
80 | }) | ||
77 | 81 | ||
78 | return new Date(timestamp).toISOString() | 82 | stream.once('close', () => res()) |
83 | }) | ||
79 | } | 84 | } |
80 | 85 | ||
81 | // Thanks: https://stackoverflow.com/a/37014317 | 86 | // Thanks: https://stackoverflow.com/a/37014317 |
82 | function getNewestFile (files: string[], basePath: string) { | 87 | async function getNewestFile (files: string[], basePath: string) { |
83 | const out = [] | 88 | const sorted = await mtimeSortFilesDesc(files, basePath) |
84 | 89 | ||
85 | files.forEach(file => { | 90 | return (sorted.length > 0) ? sorted[ 0 ].file : '' |
86 | const stats = statSync(basePath + '/' + file) | 91 | } |
87 | if (stats.isFile()) out.push({ file, mtime: stats.mtime.getTime() }) | 92 | |
88 | }) | 93 | function toTimeFormat (time: string) { |
94 | const timestamp = Date.parse(time) | ||
89 | 95 | ||
90 | out.sort((a, b) => b.mtime - a.mtime) | 96 | if (isNaN(timestamp) === true) return 'Unknown date' |
91 | 97 | ||
92 | return (out.length > 0) ? out[ 0 ].file : '' | 98 | return new Date(timestamp).toISOString() |
93 | } | 99 | } |
diff --git a/server/controllers/api/server/index.ts b/server/controllers/api/server/index.ts index 814248e5f..de09588df 100644 --- a/server/controllers/api/server/index.ts +++ b/server/controllers/api/server/index.ts | |||
@@ -4,6 +4,7 @@ import { statsRouter } from './stats' | |||
4 | import { serverRedundancyRouter } from './redundancy' | 4 | import { serverRedundancyRouter } from './redundancy' |
5 | import { serverBlocklistRouter } from './server-blocklist' | 5 | import { serverBlocklistRouter } from './server-blocklist' |
6 | import { contactRouter } from './contact' | 6 | import { contactRouter } from './contact' |
7 | import { logsRouter } from './logs' | ||
7 | 8 | ||
8 | const serverRouter = express.Router() | 9 | const serverRouter = express.Router() |
9 | 10 | ||
@@ -12,6 +13,7 @@ serverRouter.use('/', serverRedundancyRouter) | |||
12 | serverRouter.use('/', statsRouter) | 13 | serverRouter.use('/', statsRouter) |
13 | serverRouter.use('/', serverBlocklistRouter) | 14 | serverRouter.use('/', serverBlocklistRouter) |
14 | serverRouter.use('/', contactRouter) | 15 | serverRouter.use('/', contactRouter) |
16 | serverRouter.use('/', logsRouter) | ||
15 | 17 | ||
16 | // --------------------------------------------------------------------------- | 18 | // --------------------------------------------------------------------------- |
17 | 19 | ||
diff --git a/server/controllers/api/server/logs.ts b/server/controllers/api/server/logs.ts new file mode 100644 index 000000000..c551c67e3 --- /dev/null +++ b/server/controllers/api/server/logs.ts | |||
@@ -0,0 +1,90 @@ | |||
1 | import * as express from 'express' | ||
2 | import { UserRight } from '../../../../shared/models/users' | ||
3 | import { asyncMiddleware, authenticate, ensureUserHasRight } from '../../../middlewares' | ||
4 | import { mtimeSortFilesDesc } from '../../../../shared/utils/logs/logs' | ||
5 | import { readdir } from 'fs-extra' | ||
6 | import { CONFIG, MAX_LOGS_OUTPUT_CHARACTERS } from '../../../initializers' | ||
7 | import { createInterface } from 'readline' | ||
8 | import { createReadStream } from 'fs' | ||
9 | import { join } from 'path' | ||
10 | import { getLogsValidator } from '../../../middlewares/validators/logs' | ||
11 | import { LogLevel } from '../../../../shared/models/server/log-level.type' | ||
12 | |||
13 | const logsRouter = express.Router() | ||
14 | |||
15 | logsRouter.get('/logs', | ||
16 | authenticate, | ||
17 | ensureUserHasRight(UserRight.MANAGE_LOGS), | ||
18 | getLogsValidator, | ||
19 | asyncMiddleware(getLogs) | ||
20 | ) | ||
21 | |||
22 | // --------------------------------------------------------------------------- | ||
23 | |||
24 | export { | ||
25 | logsRouter | ||
26 | } | ||
27 | |||
28 | // --------------------------------------------------------------------------- | ||
29 | |||
30 | async function getLogs (req: express.Request, res: express.Response) { | ||
31 | const logFiles = await readdir(CONFIG.STORAGE.LOG_DIR) | ||
32 | const sortedLogFiles = await mtimeSortFilesDesc(logFiles, CONFIG.STORAGE.LOG_DIR) | ||
33 | let currentSize = 0 | ||
34 | |||
35 | const startDate = new Date(req.query.startDate) | ||
36 | const endDate = req.query.endDate ? new Date(req.query.endDate) : new Date() | ||
37 | const level: LogLevel = req.query.level || 'info' | ||
38 | |||
39 | let output = '' | ||
40 | |||
41 | for (const meta of sortedLogFiles) { | ||
42 | const path = join(CONFIG.STORAGE.LOG_DIR, meta.file) | ||
43 | |||
44 | const result = await getOutputFromFile(path, startDate, endDate, level, currentSize) | ||
45 | if (!result.output) break | ||
46 | |||
47 | output = output + result.output | ||
48 | currentSize = result.currentSize | ||
49 | |||
50 | if (currentSize > MAX_LOGS_OUTPUT_CHARACTERS) break | ||
51 | } | ||
52 | |||
53 | return res.json(output).end() | ||
54 | } | ||
55 | |||
56 | function getOutputFromFile (path: string, startDate: Date, endDate: Date, level: LogLevel, currentSize: number) { | ||
57 | const startTime = startDate.getTime() | ||
58 | const endTime = endDate.getTime() | ||
59 | |||
60 | const logsLevel: { [ id in LogLevel ]: number } = { | ||
61 | debug: 0, | ||
62 | info: 1, | ||
63 | warn: 2, | ||
64 | error: 3 | ||
65 | } | ||
66 | |||
67 | return new Promise<{ output: string, currentSize: number }>(res => { | ||
68 | const stream = createReadStream(path) | ||
69 | let output = '' | ||
70 | |||
71 | stream.once('close', () => res({ output, currentSize })) | ||
72 | |||
73 | const rl = createInterface({ | ||
74 | input: stream | ||
75 | }) | ||
76 | |||
77 | rl.on('line', line => { | ||
78 | const log = JSON.parse(line) | ||
79 | |||
80 | const logTime = new Date(log.timestamp).getTime() | ||
81 | if (logTime >= startTime && logTime <= endTime && logsLevel[log.level] >= logsLevel[level]) { | ||
82 | output += line | ||
83 | |||
84 | currentSize += line.length | ||
85 | |||
86 | if (currentSize > MAX_LOGS_OUTPUT_CHARACTERS) stream.close() | ||
87 | } | ||
88 | }) | ||
89 | }) | ||
90 | } | ||
diff --git a/server/helpers/custom-validators/logs.ts b/server/helpers/custom-validators/logs.ts new file mode 100644 index 000000000..30d0ce262 --- /dev/null +++ b/server/helpers/custom-validators/logs.ts | |||
@@ -0,0 +1,14 @@ | |||
1 | import { exists } from './misc' | ||
2 | import { LogLevel } from '../../../shared/models/server/log-level.type' | ||
3 | |||
4 | const logLevels: LogLevel[] = [ 'debug', 'info', 'warn', 'error' ] | ||
5 | |||
6 | function isValidLogLevel (value: any) { | ||
7 | return exists(value) && logLevels.indexOf(value) !== -1 | ||
8 | } | ||
9 | |||
10 | // --------------------------------------------------------------------------- | ||
11 | |||
12 | export { | ||
13 | isValidLogLevel | ||
14 | } | ||
diff --git a/server/helpers/logger.ts b/server/helpers/logger.ts index 203e637a8..f8a142718 100644 --- a/server/helpers/logger.ts +++ b/server/helpers/logger.ts | |||
@@ -3,10 +3,12 @@ import { mkdirpSync } from 'fs-extra' | |||
3 | import * as path from 'path' | 3 | import * as path from 'path' |
4 | import * as winston from 'winston' | 4 | import * as winston from 'winston' |
5 | import { CONFIG } from '../initializers' | 5 | import { CONFIG } from '../initializers' |
6 | import { omit } from 'lodash' | ||
6 | 7 | ||
7 | const label = CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT | 8 | const label = CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT |
8 | 9 | ||
9 | // Create the directory if it does not exist | 10 | // Create the directory if it does not exist |
11 | // FIXME: use async | ||
10 | mkdirpSync(CONFIG.STORAGE.LOG_DIR) | 12 | mkdirpSync(CONFIG.STORAGE.LOG_DIR) |
11 | 13 | ||
12 | function loggerReplacer (key: string, value: any) { | 14 | function loggerReplacer (key: string, value: any) { |
@@ -22,13 +24,10 @@ function loggerReplacer (key: string, value: any) { | |||
22 | } | 24 | } |
23 | 25 | ||
24 | const consoleLoggerFormat = winston.format.printf(info => { | 26 | const consoleLoggerFormat = winston.format.printf(info => { |
25 | const obj = { | 27 | const obj = omit(info, 'label', 'timestamp', 'level', 'message') |
26 | meta: info.meta, | ||
27 | err: info.err, | ||
28 | sql: info.sql | ||
29 | } | ||
30 | 28 | ||
31 | let additionalInfos = JSON.stringify(obj, loggerReplacer, 2) | 29 | let additionalInfos = JSON.stringify(obj, loggerReplacer, 2) |
30 | |||
32 | if (additionalInfos === undefined || additionalInfos === '{}') additionalInfos = '' | 31 | if (additionalInfos === undefined || additionalInfos === '{}') additionalInfos = '' |
33 | else additionalInfos = ' ' + additionalInfos | 32 | else additionalInfos = ' ' + additionalInfos |
34 | 33 | ||
@@ -57,7 +56,7 @@ const logger = winston.createLogger({ | |||
57 | filename: path.join(CONFIG.STORAGE.LOG_DIR, 'peertube.log'), | 56 | filename: path.join(CONFIG.STORAGE.LOG_DIR, 'peertube.log'), |
58 | handleExceptions: true, | 57 | handleExceptions: true, |
59 | maxsize: 1024 * 1024 * 12, | 58 | maxsize: 1024 * 1024 * 12, |
60 | maxFiles: 5, | 59 | maxFiles: 20, |
61 | format: winston.format.combine( | 60 | format: winston.format.combine( |
62 | winston.format.timestamp(), | 61 | winston.format.timestamp(), |
63 | jsonLoggerFormat | 62 | jsonLoggerFormat |
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts index 3f02572db..739ea5502 100644 --- a/server/initializers/constants.ts +++ b/server/initializers/constants.ts | |||
@@ -730,6 +730,8 @@ const FEEDS = { | |||
730 | COUNT: 20 | 730 | COUNT: 20 |
731 | } | 731 | } |
732 | 732 | ||
733 | const MAX_LOGS_OUTPUT_CHARACTERS = 10 * 1000 * 1000 | ||
734 | |||
733 | // --------------------------------------------------------------------------- | 735 | // --------------------------------------------------------------------------- |
734 | 736 | ||
735 | const TRACKER_RATE_LIMITS = { | 737 | const TRACKER_RATE_LIMITS = { |
@@ -819,6 +821,7 @@ export { | |||
819 | STATIC_PATHS, | 821 | STATIC_PATHS, |
820 | VIDEO_IMPORT_TIMEOUT, | 822 | VIDEO_IMPORT_TIMEOUT, |
821 | VIDEO_PLAYLIST_TYPES, | 823 | VIDEO_PLAYLIST_TYPES, |
824 | MAX_LOGS_OUTPUT_CHARACTERS, | ||
822 | ACTIVITY_PUB, | 825 | ACTIVITY_PUB, |
823 | ACTIVITY_PUB_ACTOR_TYPES, | 826 | ACTIVITY_PUB_ACTOR_TYPES, |
824 | THUMBNAILS_SIZE, | 827 | THUMBNAILS_SIZE, |
diff --git a/server/middlewares/validators/logs.ts b/server/middlewares/validators/logs.ts new file mode 100644 index 000000000..7380c6edd --- /dev/null +++ b/server/middlewares/validators/logs.ts | |||
@@ -0,0 +1,31 @@ | |||
1 | import * as express from 'express' | ||
2 | import { logger } from '../../helpers/logger' | ||
3 | import { areValidationErrors } from './utils' | ||
4 | import { isDateValid } from '../../helpers/custom-validators/misc' | ||
5 | import { query } from 'express-validator/check' | ||
6 | import { isValidLogLevel } from '../../helpers/custom-validators/logs' | ||
7 | |||
8 | const getLogsValidator = [ | ||
9 | query('startDate') | ||
10 | .custom(isDateValid).withMessage('Should have a valid start date'), | ||
11 | query('level') | ||
12 | .optional() | ||
13 | .custom(isValidLogLevel).withMessage('Should have a valid level'), | ||
14 | query('endDate') | ||
15 | .optional() | ||
16 | .custom(isDateValid).withMessage('Should have a valid end date'), | ||
17 | |||
18 | (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
19 | logger.debug('Checking getLogsValidator parameters.', { parameters: req.query }) | ||
20 | |||
21 | if (areValidationErrors(req, res)) return | ||
22 | |||
23 | return next() | ||
24 | } | ||
25 | ] | ||
26 | |||
27 | // --------------------------------------------------------------------------- | ||
28 | |||
29 | export { | ||
30 | getLogsValidator | ||
31 | } | ||
diff --git a/server/middlewares/validators/videos/videos.ts b/server/middlewares/validators/videos/videos.ts index b70abf429..e247db708 100644 --- a/server/middlewares/validators/videos/videos.ts +++ b/server/middlewares/validators/videos/videos.ts | |||
@@ -14,18 +14,18 @@ import { | |||
14 | } from '../../../helpers/custom-validators/misc' | 14 | } from '../../../helpers/custom-validators/misc' |
15 | import { | 15 | import { |
16 | checkUserCanManageVideo, | 16 | checkUserCanManageVideo, |
17 | isVideoOriginallyPublishedAtValid, | 17 | doesVideoChannelOfAccountExist, |
18 | doesVideoExist, | ||
18 | isScheduleVideoUpdatePrivacyValid, | 19 | isScheduleVideoUpdatePrivacyValid, |
19 | isVideoCategoryValid, | 20 | isVideoCategoryValid, |
20 | doesVideoChannelOfAccountExist, | ||
21 | isVideoDescriptionValid, | 21 | isVideoDescriptionValid, |
22 | doesVideoExist, | ||
23 | isVideoFile, | 22 | isVideoFile, |
24 | isVideoFilterValid, | 23 | isVideoFilterValid, |
25 | isVideoImage, | 24 | isVideoImage, |
26 | isVideoLanguageValid, | 25 | isVideoLanguageValid, |
27 | isVideoLicenceValid, | 26 | isVideoLicenceValid, |
28 | isVideoNameValid, | 27 | isVideoNameValid, |
28 | isVideoOriginallyPublishedAtValid, | ||
29 | isVideoPrivacyValid, | 29 | isVideoPrivacyValid, |
30 | isVideoSupportValid, | 30 | isVideoSupportValid, |
31 | isVideoTagsValid | 31 | isVideoTagsValid |
@@ -37,10 +37,8 @@ import { authenticatePromiseIfNeeded } from '../../oauth' | |||
37 | import { areValidationErrors } from '../utils' | 37 | import { areValidationErrors } from '../utils' |
38 | import { cleanUpReqFiles } from '../../../helpers/express-utils' | 38 | import { cleanUpReqFiles } from '../../../helpers/express-utils' |
39 | import { VideoModel } from '../../../models/video/video' | 39 | import { VideoModel } from '../../../models/video/video' |
40 | import { UserModel } from '../../../models/account/user' | ||
41 | import { checkUserCanTerminateOwnershipChange, doesChangeVideoOwnershipExist } from '../../../helpers/custom-validators/video-ownership' | 40 | import { checkUserCanTerminateOwnershipChange, doesChangeVideoOwnershipExist } from '../../../helpers/custom-validators/video-ownership' |
42 | import { VideoChangeOwnershipAccept } from '../../../../shared/models/videos/video-change-ownership-accept.model' | 41 | import { VideoChangeOwnershipAccept } from '../../../../shared/models/videos/video-change-ownership-accept.model' |
43 | import { VideoChangeOwnershipModel } from '../../../models/video/video-change-ownership' | ||
44 | import { AccountModel } from '../../../models/account/account' | 42 | import { AccountModel } from '../../../models/account/account' |
45 | import { VideoFetchType } from '../../../helpers/video' | 43 | import { VideoFetchType } from '../../../helpers/video' |
46 | import { isNSFWQueryValid, isNumberArray, isStringArray } from '../../../helpers/custom-validators/search' | 44 | import { isNSFWQueryValid, isNumberArray, isStringArray } from '../../../helpers/custom-validators/search' |
diff --git a/server/tests/api/check-params/index.ts b/server/tests/api/check-params/index.ts index ca51cd39a..bdac95025 100644 --- a/server/tests/api/check-params/index.ts +++ b/server/tests/api/check-params/index.ts | |||
@@ -4,6 +4,7 @@ import './config' | |||
4 | import './contact-form' | 4 | import './contact-form' |
5 | import './follows' | 5 | import './follows' |
6 | import './jobs' | 6 | import './jobs' |
7 | import './logs' | ||
7 | import './redundancy' | 8 | import './redundancy' |
8 | import './search' | 9 | import './search' |
9 | import './services' | 10 | import './services' |
diff --git a/server/tests/api/check-params/logs.ts b/server/tests/api/check-params/logs.ts new file mode 100644 index 000000000..d6a40da61 --- /dev/null +++ b/server/tests/api/check-params/logs.ts | |||
@@ -0,0 +1,117 @@ | |||
1 | /* tslint:disable:no-unused-expression */ | ||
2 | |||
3 | import 'mocha' | ||
4 | |||
5 | import { | ||
6 | createUser, | ||
7 | flushTests, | ||
8 | killallServers, | ||
9 | runServer, | ||
10 | ServerInfo, | ||
11 | setAccessTokensToServers, | ||
12 | userLogin | ||
13 | } from '../../../../shared/utils' | ||
14 | import { makeGetRequest } from '../../../../shared/utils/requests/requests' | ||
15 | |||
16 | describe('Test logs API validators', function () { | ||
17 | const path = '/api/v1/server/logs' | ||
18 | let server: ServerInfo | ||
19 | let userAccessToken = '' | ||
20 | |||
21 | // --------------------------------------------------------------- | ||
22 | |||
23 | before(async function () { | ||
24 | this.timeout(120000) | ||
25 | |||
26 | await flushTests() | ||
27 | |||
28 | server = await runServer(1) | ||
29 | |||
30 | await setAccessTokensToServers([ server ]) | ||
31 | |||
32 | const user = { | ||
33 | username: 'user1', | ||
34 | password: 'my super password' | ||
35 | } | ||
36 | await createUser(server.url, server.accessToken, user.username, user.password) | ||
37 | userAccessToken = await userLogin(server, user) | ||
38 | }) | ||
39 | |||
40 | describe('When getting logs', function () { | ||
41 | |||
42 | it('Should fail with a non authenticated user', async function () { | ||
43 | await makeGetRequest({ | ||
44 | url: server.url, | ||
45 | path, | ||
46 | statusCodeExpected: 401 | ||
47 | }) | ||
48 | }) | ||
49 | |||
50 | it('Should fail with a non admin user', async function () { | ||
51 | await makeGetRequest({ | ||
52 | url: server.url, | ||
53 | path, | ||
54 | token: userAccessToken, | ||
55 | statusCodeExpected: 403 | ||
56 | }) | ||
57 | }) | ||
58 | |||
59 | it('Should fail with a missing startDate query', async function () { | ||
60 | await makeGetRequest({ | ||
61 | url: server.url, | ||
62 | path, | ||
63 | token: server.accessToken, | ||
64 | statusCodeExpected: 400 | ||
65 | }) | ||
66 | }) | ||
67 | |||
68 | it('Should fail with a bad startDate query', async function () { | ||
69 | await makeGetRequest({ | ||
70 | url: server.url, | ||
71 | path, | ||
72 | token: server.accessToken, | ||
73 | query: { startDate: 'toto' }, | ||
74 | statusCodeExpected: 400 | ||
75 | }) | ||
76 | }) | ||
77 | |||
78 | it('Should fail with a bad endDate query', async function () { | ||
79 | await makeGetRequest({ | ||
80 | url: server.url, | ||
81 | path, | ||
82 | token: server.accessToken, | ||
83 | query: { startDate: new Date().toISOString(), endDate: 'toto' }, | ||
84 | statusCodeExpected: 400 | ||
85 | }) | ||
86 | }) | ||
87 | |||
88 | it('Should fail with a bad level parameter', async function () { | ||
89 | await makeGetRequest({ | ||
90 | url: server.url, | ||
91 | path, | ||
92 | token: server.accessToken, | ||
93 | query: { startDate: new Date().toISOString(), level: 'toto' }, | ||
94 | statusCodeExpected: 400 | ||
95 | }) | ||
96 | }) | ||
97 | |||
98 | it('Should succeed with the correct params', async function () { | ||
99 | await makeGetRequest({ | ||
100 | url: server.url, | ||
101 | path, | ||
102 | token: server.accessToken, | ||
103 | query: { startDate: new Date().toISOString() }, | ||
104 | statusCodeExpected: 200 | ||
105 | }) | ||
106 | }) | ||
107 | }) | ||
108 | |||
109 | after(async function () { | ||
110 | killallServers([ server ]) | ||
111 | |||
112 | // Keep the logs if the test failed | ||
113 | if (this['ok']) { | ||
114 | await flushTests() | ||
115 | } | ||
116 | }) | ||
117 | }) | ||
diff --git a/server/tests/api/server/index.ts b/server/tests/api/server/index.ts index 4e53074ab..94c15e0d0 100644 --- a/server/tests/api/server/index.ts +++ b/server/tests/api/server/index.ts | |||
@@ -6,6 +6,7 @@ import './follows' | |||
6 | import './follows-moderation' | 6 | import './follows-moderation' |
7 | import './handle-down' | 7 | import './handle-down' |
8 | import './jobs' | 8 | import './jobs' |
9 | import './logs' | ||
9 | import './reverse-proxy' | 10 | import './reverse-proxy' |
10 | import './stats' | 11 | import './stats' |
11 | import './tracker' | 12 | import './tracker' |
diff --git a/server/tests/api/server/logs.ts b/server/tests/api/server/logs.ts new file mode 100644 index 000000000..05b0308de --- /dev/null +++ b/server/tests/api/server/logs.ts | |||
@@ -0,0 +1,92 @@ | |||
1 | /* tslint:disable:no-unused-expression */ | ||
2 | |||
3 | import * as chai from 'chai' | ||
4 | import 'mocha' | ||
5 | import { flushTests, killallServers, runServer, ServerInfo, setAccessTokensToServers } from '../../../../shared/utils/index' | ||
6 | import { waitJobs } from '../../../../shared/utils/server/jobs' | ||
7 | import { uploadVideo } from '../../../../shared/utils/videos/videos' | ||
8 | import { getLogs } from '../../../../shared/utils/logs/logs' | ||
9 | |||
10 | const expect = chai.expect | ||
11 | |||
12 | describe('Test logs', function () { | ||
13 | let server: ServerInfo | ||
14 | |||
15 | before(async function () { | ||
16 | this.timeout(30000) | ||
17 | |||
18 | await flushTests() | ||
19 | |||
20 | server = await runServer(1) | ||
21 | await setAccessTokensToServers([ server ]) | ||
22 | }) | ||
23 | |||
24 | it('Should get logs with a start date', async function () { | ||
25 | this.timeout(10000) | ||
26 | |||
27 | await uploadVideo(server.url, server.accessToken, { name: 'video 1' }) | ||
28 | await waitJobs([ server ]) | ||
29 | |||
30 | const now = new Date() | ||
31 | |||
32 | await uploadVideo(server.url, server.accessToken, { name: 'video 2' }) | ||
33 | await waitJobs([ server ]) | ||
34 | |||
35 | const res = await getLogs(server.url, server.accessToken, now) | ||
36 | const logsString = JSON.stringify(res.body) | ||
37 | |||
38 | expect(logsString.includes('video 1')).to.be.false | ||
39 | expect(logsString.includes('video 2')).to.be.true | ||
40 | }) | ||
41 | |||
42 | it('Should get logs with an end date', async function () { | ||
43 | this.timeout(10000) | ||
44 | |||
45 | await uploadVideo(server.url, server.accessToken, { name: 'video 3' }) | ||
46 | await waitJobs([ server ]) | ||
47 | |||
48 | const now1 = new Date() | ||
49 | |||
50 | await uploadVideo(server.url, server.accessToken, { name: 'video 4' }) | ||
51 | await waitJobs([ server ]) | ||
52 | |||
53 | const now2 = new Date() | ||
54 | |||
55 | await uploadVideo(server.url, server.accessToken, { name: 'video 5' }) | ||
56 | await waitJobs([ server ]) | ||
57 | |||
58 | const res = await getLogs(server.url, server.accessToken, now1, now2) | ||
59 | const logsString = JSON.stringify(res.body) | ||
60 | |||
61 | expect(logsString.includes('video 3')).to.be.false | ||
62 | expect(logsString.includes('video 4')).to.be.true | ||
63 | expect(logsString.includes('video 5')).to.be.false | ||
64 | }) | ||
65 | |||
66 | it('Should get filter by level', async function () { | ||
67 | this.timeout(10000) | ||
68 | |||
69 | const now = new Date() | ||
70 | |||
71 | await uploadVideo(server.url, server.accessToken, { name: 'video 6' }) | ||
72 | await waitJobs([ server ]) | ||
73 | |||
74 | { | ||
75 | const res = await getLogs(server.url, server.accessToken, now, undefined, 'info') | ||
76 | const logsString = JSON.stringify(res.body) | ||
77 | |||
78 | expect(logsString.includes('video 6')).to.be.true | ||
79 | } | ||
80 | |||
81 | { | ||
82 | const res = await getLogs(server.url, server.accessToken, now, undefined, 'warn') | ||
83 | const logsString = JSON.stringify(res.body) | ||
84 | |||
85 | expect(logsString.includes('video 6')).to.be.false | ||
86 | } | ||
87 | }) | ||
88 | |||
89 | after(async function () { | ||
90 | killallServers([ server ]) | ||
91 | }) | ||
92 | }) | ||
diff --git a/shared/models/server/log-level.type.ts b/shared/models/server/log-level.type.ts new file mode 100644 index 000000000..ce91559e3 --- /dev/null +++ b/shared/models/server/log-level.type.ts | |||
@@ -0,0 +1 @@ | |||
export type LogLevel = 'debug' | 'info' | 'warn' | 'error' | |||
diff --git a/shared/models/users/user-right.enum.ts b/shared/models/users/user-right.enum.ts index eaa064bd9..5ec255ea5 100644 --- a/shared/models/users/user-right.enum.ts +++ b/shared/models/users/user-right.enum.ts | |||
@@ -5,6 +5,8 @@ export enum UserRight { | |||
5 | 5 | ||
6 | MANAGE_SERVER_FOLLOW, | 6 | MANAGE_SERVER_FOLLOW, |
7 | 7 | ||
8 | MANAGE_LOGS, | ||
9 | |||
8 | MANAGE_SERVER_REDUNDANCY, | 10 | MANAGE_SERVER_REDUNDANCY, |
9 | 11 | ||
10 | MANAGE_VIDEO_ABUSES, | 12 | MANAGE_VIDEO_ABUSES, |
diff --git a/shared/utils/logs/logs.ts b/shared/utils/logs/logs.ts new file mode 100644 index 000000000..21adace82 --- /dev/null +++ b/shared/utils/logs/logs.ts | |||
@@ -0,0 +1,41 @@ | |||
1 | // Thanks: https://stackoverflow.com/a/37014317 | ||
2 | import { stat } from 'fs-extra' | ||
3 | import { makeGetRequest } from '../requests/requests' | ||
4 | import { LogLevel } from '../../models/server/log-level.type' | ||
5 | |||
6 | async function mtimeSortFilesDesc (files: string[], basePath: string) { | ||
7 | const promises = [] | ||
8 | const out: { file: string, mtime: number }[] = [] | ||
9 | |||
10 | for (const file of files) { | ||
11 | const p = stat(basePath + '/' + file) | ||
12 | .then(stats => { | ||
13 | if (stats.isFile()) out.push({ file, mtime: stats.mtime.getTime() }) | ||
14 | }) | ||
15 | |||
16 | promises.push(p) | ||
17 | } | ||
18 | |||
19 | await Promise.all(promises) | ||
20 | |||
21 | out.sort((a, b) => b.mtime - a.mtime) | ||
22 | |||
23 | return out | ||
24 | } | ||
25 | |||
26 | function getLogs (url: string, accessToken: string, startDate: Date, endDate?: Date, level?: LogLevel) { | ||
27 | const path = '/api/v1/server/logs' | ||
28 | |||
29 | return makeGetRequest({ | ||
30 | url, | ||
31 | path, | ||
32 | token: accessToken, | ||
33 | query: { startDate, endDate, level }, | ||
34 | statusCodeExpected: 200 | ||
35 | }) | ||
36 | } | ||
37 | |||
38 | export { | ||
39 | mtimeSortFilesDesc, | ||
40 | getLogs | ||
41 | } | ||