diff options
author | Chocobozzz <me@florianbigard.com> | 2019-12-11 14:14:01 +0100 |
---|---|---|
committer | Chocobozzz <me@florianbigard.com> | 2019-12-11 14:14:01 +0100 |
commit | 566c125d6eee3bd907404523d94e1e0b5e403a46 (patch) | |
tree | c477cdd2ba745015d80052968c37927b1bca1254 | |
parent | 92e0f42e8ce5f1ab5e4023900b8194627231a11b (diff) | |
download | PeerTube-566c125d6eee3bd907404523d94e1e0b5e403a46.tar.gz PeerTube-566c125d6eee3bd907404523d94e1e0b5e403a46.tar.zst PeerTube-566c125d6eee3bd907404523d94e1e0b5e403a46.zip |
Serve audit logs to client
18 files changed, 272 insertions, 72 deletions
diff --git a/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.html b/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.html index 627437053..30eb2dbde 100644 --- a/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.html +++ b/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.html | |||
@@ -15,7 +15,7 @@ | |||
15 | 15 | ||
16 | <ng-template pTemplate="body" let-expanded="expanded" let-videoAbuse> | 16 | <ng-template pTemplate="body" let-expanded="expanded" let-videoAbuse> |
17 | <tr> | 17 | <tr> |
18 | <td> | 18 | <td class="expand-cell"> |
19 | <span class="expander" [pRowToggler]="videoAbuse"> | 19 | <span class="expander" [pRowToggler]="videoAbuse"> |
20 | <i [ngClass]="expanded ? 'glyphicon glyphicon-menu-down' : 'glyphicon glyphicon-menu-right'"></i> | 20 | <i [ngClass]="expanded ? 'glyphicon glyphicon-menu-down' : 'glyphicon glyphicon-menu-right'"></i> |
21 | </span> | 21 | </span> |
diff --git a/client/src/app/+admin/moderation/video-blacklist-list/video-blacklist-list.component.html b/client/src/app/+admin/moderation/video-blacklist-list/video-blacklist-list.component.html index 608dff2d8..a0b89acc6 100644 --- a/client/src/app/+admin/moderation/video-blacklist-list/video-blacklist-list.component.html +++ b/client/src/app/+admin/moderation/video-blacklist-list/video-blacklist-list.component.html | |||
@@ -15,7 +15,7 @@ | |||
15 | 15 | ||
16 | <ng-template pTemplate="body" let-videoBlacklist let-expanded="expanded"> | 16 | <ng-template pTemplate="body" let-videoBlacklist let-expanded="expanded"> |
17 | <tr> | 17 | <tr> |
18 | <td> | 18 | <td class="expand-cell"> |
19 | <span *ngIf="videoBlacklist.reason" class="expander" [pRowToggler]="videoBlacklist"> | 19 | <span *ngIf="videoBlacklist.reason" class="expander" [pRowToggler]="videoBlacklist"> |
20 | <i [ngClass]="expanded ? 'glyphicon glyphicon-menu-down' : 'glyphicon glyphicon-menu-right'"></i> | 20 | <i [ngClass]="expanded ? 'glyphicon glyphicon-menu-down' : 'glyphicon glyphicon-menu-right'"></i> |
21 | </span> | 21 | </span> |
diff --git a/client/src/app/+admin/system/jobs/jobs.component.html b/client/src/app/+admin/system/jobs/jobs.component.html index cd26257dd..de43b6448 100644 --- a/client/src/app/+admin/system/jobs/jobs.component.html +++ b/client/src/app/+admin/system/jobs/jobs.component.html | |||
@@ -38,7 +38,7 @@ | |||
38 | 38 | ||
39 | <ng-template pTemplate="body" let-expanded="expanded" let-job> | 39 | <ng-template pTemplate="body" let-expanded="expanded" let-job> |
40 | <tr> | 40 | <tr> |
41 | <td> | 41 | <td class="expand-cell"> |
42 | <span class="expander" [pRowToggler]="job"> | 42 | <span class="expander" [pRowToggler]="job"> |
43 | <i [ngClass]="expanded ? 'glyphicon glyphicon-menu-down' : 'glyphicon glyphicon-menu-right'"></i> | 43 | <i [ngClass]="expanded ? 'glyphicon glyphicon-menu-down' : 'glyphicon glyphicon-menu-right'"></i> |
44 | </span> | 44 | </span> |
diff --git a/client/src/app/+admin/system/logs/log-row.model.ts b/client/src/app/+admin/system/logs/log-row.model.ts index 9bc7dafdd..b22581b5a 100644 --- a/client/src/app/+admin/system/logs/log-row.model.ts +++ b/client/src/app/+admin/system/logs/log-row.model.ts | |||
@@ -8,6 +8,10 @@ export class LogRow { | |||
8 | message: string | 8 | message: string |
9 | meta: string | 9 | meta: string |
10 | 10 | ||
11 | by: string | ||
12 | domain: string | ||
13 | action: string | ||
14 | |||
11 | constructor (row: any) { | 15 | constructor (row: any) { |
12 | this.date = new Date(row.timestamp) | 16 | this.date = new Date(row.timestamp) |
13 | this.localeDate = this.date.toLocaleString() | 17 | this.localeDate = this.date.toLocaleString() |
@@ -17,5 +21,20 @@ export class LogRow { | |||
17 | const metaObj = omit(row, 'timestamp', 'level', 'message', 'label') | 21 | const metaObj = omit(row, 'timestamp', 'level', 'message', 'label') |
18 | 22 | ||
19 | if (Object.keys(metaObj).length !== 0) this.meta = JSON.stringify(metaObj, undefined, 2) | 23 | if (Object.keys(metaObj).length !== 0) this.meta = JSON.stringify(metaObj, undefined, 2) |
24 | |||
25 | if (row.level === 'audit') { | ||
26 | try { | ||
27 | const message = JSON.parse(row.message) | ||
28 | |||
29 | this.by = message.user | ||
30 | this.domain = message.domain | ||
31 | this.action = message.action | ||
32 | |||
33 | this.meta = JSON.stringify(message, null, 2) | ||
34 | this.message = '' | ||
35 | } catch (err) { | ||
36 | console.error('Cannot parse audit message.', err) | ||
37 | } | ||
38 | } | ||
20 | } | 39 | } |
21 | } | 40 | } |
diff --git a/client/src/app/+admin/system/logs/logs.component.html b/client/src/app/+admin/system/logs/logs.component.html index 45723a655..ddad1314f 100644 --- a/client/src/app/+admin/system/logs/logs.component.html +++ b/client/src/app/+admin/system/logs/logs.component.html | |||
@@ -1,11 +1,17 @@ | |||
1 | <div class="header"> | 1 | <div class="header"> |
2 | <div class="peertube-select-container"> | 2 | <div class="peertube-select-container"> |
3 | <select [(ngModel)]="logType" (ngModelChange)="refresh()"> | ||
4 | <option *ngFor="let logTypeChoice of logTypeChoices" [value]="logTypeChoice.id">{{ logTypeChoice.label }}</option> | ||
5 | </select> | ||
6 | </div> | ||
7 | |||
8 | <div class="peertube-select-container"> | ||
3 | <select [(ngModel)]="startDate" (ngModelChange)="refresh()"> | 9 | <select [(ngModel)]="startDate" (ngModelChange)="refresh()"> |
4 | <option *ngFor="let timeChoice of timeChoices" [value]="timeChoice.id">{{ timeChoice.label }}</option> | 10 | <option *ngFor="let timeChoice of timeChoices" [value]="timeChoice.id">{{ timeChoice.label }}</option> |
5 | </select> | 11 | </select> |
6 | </div> | 12 | </div> |
7 | 13 | ||
8 | <div class="peertube-select-container"> | 14 | <div class="peertube-select-container" *ngIf="!isAuditLog()"> |
9 | <select [(ngModel)]="level" (ngModelChange)="refresh()"> | 15 | <select [(ngModel)]="level" (ngModelChange)="refresh()"> |
10 | <option *ngFor="let levelChoice of levelChoices" [value]="levelChoice.id">{{ levelChoice.label }}</option> | 16 | <option *ngFor="let levelChoice of levelChoices" [value]="levelChoice.id">{{ levelChoice.label }}</option> |
11 | </select> | 17 | </select> |
@@ -23,9 +29,12 @@ | |||
23 | 29 | ||
24 | <span class="log-date">[{{ log.localeDate }}]</span> | 30 | <span class="log-date">[{{ log.localeDate }}]</span> |
25 | 31 | ||
32 | <strong class="log-by" *ngIf="log.by" i18n>By {{ log.by }} -></strong> | ||
33 | <strong class="log-domain-action" *ngIf="log.domain">{{ log.domain }} -> {{ log.action }}</strong> | ||
34 | |||
26 | {{ log.message }} | 35 | {{ log.message }} |
27 | 36 | ||
28 | {{ log.meta }} | 37 | <pre>{{ log.meta }}</pre> |
29 | </div> | 38 | </div> |
30 | </div> | 39 | </div> |
31 | </div> | 40 | </div> |
diff --git a/client/src/app/+admin/system/logs/logs.component.scss b/client/src/app/+admin/system/logs/logs.component.scss index 7ad2e853c..dae8b21c7 100644 --- a/client/src/app/+admin/system/logs/logs.component.scss +++ b/client/src/app/+admin/system/logs/logs.component.scss | |||
@@ -23,6 +23,10 @@ | |||
23 | margin-right: 5px; | 23 | margin-right: 5px; |
24 | } | 24 | } |
25 | 25 | ||
26 | .log-by { | ||
27 | margin: 0 5px; | ||
28 | } | ||
29 | |||
26 | .warn { | 30 | .warn { |
27 | color: $orange-color; | 31 | color: $orange-color; |
28 | } | 32 | } |
@@ -30,6 +34,12 @@ | |||
30 | .error { | 34 | .error { |
31 | color: $red; | 35 | color: $red; |
32 | } | 36 | } |
37 | |||
38 | pre { | ||
39 | margin-bottom: 5px; | ||
40 | white-space: pre-wrap; | ||
41 | word-wrap: break-word; | ||
42 | } | ||
33 | } | 43 | } |
34 | 44 | ||
35 | .header { | 45 | .header { |
diff --git a/client/src/app/+admin/system/logs/logs.component.ts b/client/src/app/+admin/system/logs/logs.component.ts index b2aca8461..b63f11953 100644 --- a/client/src/app/+admin/system/logs/logs.component.ts +++ b/client/src/app/+admin/system/logs/logs.component.ts | |||
@@ -17,9 +17,11 @@ export class LogsComponent implements OnInit { | |||
17 | logs: LogRow[] = [] | 17 | logs: LogRow[] = [] |
18 | timeChoices: { id: string, label: string }[] = [] | 18 | timeChoices: { id: string, label: string }[] = [] |
19 | levelChoices: { id: LogLevel, label: string }[] = [] | 19 | levelChoices: { id: LogLevel, label: string }[] = [] |
20 | logTypeChoices: { id: 'audit' | 'standard', label: string }[] = [] | ||
20 | 21 | ||
21 | startDate: string | 22 | startDate: string |
22 | level: LogLevel | 23 | level: LogLevel |
24 | logType: 'audit' | 'standard' | ||
23 | 25 | ||
24 | constructor ( | 26 | constructor ( |
25 | private logsService: LogsService, | 27 | private logsService: LogsService, |
@@ -30,6 +32,7 @@ export class LogsComponent implements OnInit { | |||
30 | ngOnInit (): void { | 32 | ngOnInit (): void { |
31 | this.buildTimeChoices() | 33 | this.buildTimeChoices() |
32 | this.buildLevelChoices() | 34 | this.buildLevelChoices() |
35 | this.buildLogTypeChoices() | ||
33 | 36 | ||
34 | this.load() | 37 | this.load() |
35 | } | 38 | } |
@@ -42,7 +45,7 @@ export class LogsComponent implements OnInit { | |||
42 | load () { | 45 | load () { |
43 | this.loading = true | 46 | this.loading = true |
44 | 47 | ||
45 | this.logsService.getLogs(this.level, this.startDate) | 48 | this.logsService.getLogs({ isAuditLog: this.isAuditLog(), level: this.level, startDate: this.startDate }) |
46 | .subscribe( | 49 | .subscribe( |
47 | logs => { | 50 | logs => { |
48 | this.logs = logs | 51 | this.logs = logs |
@@ -58,6 +61,10 @@ export class LogsComponent implements OnInit { | |||
58 | ) | 61 | ) |
59 | } | 62 | } |
60 | 63 | ||
64 | isAuditLog () { | ||
65 | return this.logType === 'audit' | ||
66 | } | ||
67 | |||
61 | buildTimeChoices () { | 68 | buildTimeChoices () { |
62 | const lastHour = new Date() | 69 | const lastHour = new Date() |
63 | lastHour.setHours(lastHour.getHours() - 1) | 70 | lastHour.setHours(lastHour.getHours() - 1) |
@@ -108,4 +115,19 @@ export class LogsComponent implements OnInit { | |||
108 | 115 | ||
109 | this.level = 'warn' | 116 | this.level = 'warn' |
110 | } | 117 | } |
118 | |||
119 | buildLogTypeChoices () { | ||
120 | this.logTypeChoices = [ | ||
121 | { | ||
122 | id: 'standard', | ||
123 | label: this.i18n('Standard logs') | ||
124 | }, | ||
125 | { | ||
126 | id: 'audit', | ||
127 | label: this.i18n('Audit logs') | ||
128 | } | ||
129 | ] | ||
130 | |||
131 | this.logType = 'audit' | ||
132 | } | ||
111 | } | 133 | } |
diff --git a/client/src/app/+admin/system/logs/logs.service.ts b/client/src/app/+admin/system/logs/logs.service.ts index 24b9cb6d1..41b38c7ba 100644 --- a/client/src/app/+admin/system/logs/logs.service.ts +++ b/client/src/app/+admin/system/logs/logs.service.ts | |||
@@ -10,6 +10,7 @@ import { LogLevel } from '@shared/models/server/log-level.type' | |||
10 | @Injectable() | 10 | @Injectable() |
11 | export class LogsService { | 11 | export class LogsService { |
12 | private static BASE_LOG_URL = environment.apiUrl + '/api/v1/server/logs' | 12 | private static BASE_LOG_URL = environment.apiUrl + '/api/v1/server/logs' |
13 | private static BASE_AUDIT_LOG_URL = environment.apiUrl + '/api/v1/server/audit-logs' | ||
13 | 14 | ||
14 | constructor ( | 15 | constructor ( |
15 | private authHttp: HttpClient, | 16 | private authHttp: HttpClient, |
@@ -17,14 +18,25 @@ export class LogsService { | |||
17 | private restExtractor: RestExtractor | 18 | private restExtractor: RestExtractor |
18 | ) {} | 19 | ) {} |
19 | 20 | ||
20 | getLogs (level: LogLevel, startDate: string, endDate?: string): Observable<any[]> { | 21 | getLogs (options: { |
22 | isAuditLog: boolean, | ||
23 | startDate: string, | ||
24 | level?: LogLevel, | ||
25 | endDate?: string | ||
26 | }): Observable<any[]> { | ||
27 | const { isAuditLog, startDate } = options | ||
28 | |||
21 | let params = new HttpParams() | 29 | let params = new HttpParams() |
22 | params = params.append('startDate', startDate) | 30 | params = params.append('startDate', startDate) |
23 | params = params.append('level', level) | ||
24 | 31 | ||
25 | if (endDate) params.append('endDate', endDate) | 32 | if (!isAuditLog) params = params.append('level', options.level) |
33 | if (options.endDate) params.append('endDate', options.endDate) | ||
34 | |||
35 | const path = isAuditLog | ||
36 | ? LogsService.BASE_AUDIT_LOG_URL | ||
37 | : LogsService.BASE_LOG_URL | ||
26 | 38 | ||
27 | return this.authHttp.get<any[]>(LogsService.BASE_LOG_URL, { params }) | 39 | return this.authHttp.get<any[]>(path, { params }) |
28 | .pipe( | 40 | .pipe( |
29 | map(rows => rows.map(r => new LogRow(r))), | 41 | map(rows => rows.map(r => new LogRow(r))), |
30 | catchError(err => this.restExtractor.handleError(err)) | 42 | catchError(err => this.restExtractor.handleError(err)) |
diff --git a/client/src/app/+admin/users/user-list/user-list.component.html b/client/src/app/+admin/users/user-list/user-list.component.html index 822bb53da..885335313 100644 --- a/client/src/app/+admin/users/user-list/user-list.component.html +++ b/client/src/app/+admin/users/user-list/user-list.component.html | |||
@@ -49,7 +49,7 @@ | |||
49 | <ng-template pTemplate="body" let-expanded="expanded" let-user> | 49 | <ng-template pTemplate="body" let-expanded="expanded" let-user> |
50 | 50 | ||
51 | <tr [pSelectableRow]="user" [ngClass]="{ banned: user.blocked }"> | 51 | <tr [pSelectableRow]="user" [ngClass]="{ banned: user.blocked }"> |
52 | <td> | 52 | <td class="expand-cell"> |
53 | <p-tableCheckbox [value]="user"></p-tableCheckbox> | 53 | <p-tableCheckbox [value]="user"></p-tableCheckbox> |
54 | </td> | 54 | </td> |
55 | 55 | ||
diff --git a/client/src/sass/primeng-custom.scss b/client/src/sass/primeng-custom.scss index 6c3100746..0acffef3c 100644 --- a/client/src/sass/primeng-custom.scss +++ b/client/src/sass/primeng-custom.scss | |||
@@ -37,7 +37,7 @@ p-table { | |||
37 | td { | 37 | td { |
38 | padding-left: 15px !important; | 38 | padding-left: 15px !important; |
39 | 39 | ||
40 | &:not(.action-cell) { | 40 | &:not(.action-cell):not(.expand-cell) { |
41 | overflow: hidden !important; | 41 | overflow: hidden !important; |
42 | text-overflow: ellipsis !important; | 42 | text-overflow: ellipsis !important; |
43 | white-space: nowrap !important; | 43 | white-space: nowrap !important; |
diff --git a/server/controllers/api/server/logs.ts b/server/controllers/api/server/logs.ts index e9d1f2efd..a0ca21cd5 100644 --- a/server/controllers/api/server/logs.ts +++ b/server/controllers/api/server/logs.ts | |||
@@ -3,11 +3,12 @@ import { UserRight } from '../../../../shared/models/users' | |||
3 | import { asyncMiddleware, authenticate, ensureUserHasRight } from '../../../middlewares' | 3 | import { asyncMiddleware, authenticate, ensureUserHasRight } from '../../../middlewares' |
4 | import { mtimeSortFilesDesc } from '../../../../shared/core-utils/logs/logs' | 4 | import { mtimeSortFilesDesc } from '../../../../shared/core-utils/logs/logs' |
5 | import { readdir, readFile } from 'fs-extra' | 5 | import { readdir, readFile } from 'fs-extra' |
6 | import { MAX_LOGS_OUTPUT_CHARACTERS } from '../../../initializers/constants' | 6 | import { AUDIT_LOG_FILENAME, MAX_LOGS_OUTPUT_CHARACTERS, LOG_FILENAME } from '../../../initializers/constants' |
7 | import { join } from 'path' | 7 | import { join } from 'path' |
8 | import { getLogsValidator } from '../../../middlewares/validators/logs' | 8 | import { getAuditLogsValidator, getLogsValidator } from '../../../middlewares/validators/logs' |
9 | import { LogLevel } from '../../../../shared/models/server/log-level.type' | 9 | import { LogLevel } from '../../../../shared/models/server/log-level.type' |
10 | import { CONFIG } from '../../../initializers/config' | 10 | import { CONFIG } from '../../../initializers/config' |
11 | import { logger } from '@server/helpers/logger' | ||
11 | 12 | ||
12 | const logsRouter = express.Router() | 13 | const logsRouter = express.Router() |
13 | 14 | ||
@@ -18,6 +19,13 @@ logsRouter.get('/logs', | |||
18 | asyncMiddleware(getLogs) | 19 | asyncMiddleware(getLogs) |
19 | ) | 20 | ) |
20 | 21 | ||
22 | logsRouter.get('/audit-logs', | ||
23 | authenticate, | ||
24 | ensureUserHasRight(UserRight.MANAGE_LOGS), | ||
25 | getAuditLogsValidator, | ||
26 | asyncMiddleware(getAuditLogs) | ||
27 | ) | ||
28 | |||
21 | // --------------------------------------------------------------------------- | 29 | // --------------------------------------------------------------------------- |
22 | 30 | ||
23 | export { | 31 | export { |
@@ -26,18 +34,50 @@ export { | |||
26 | 34 | ||
27 | // --------------------------------------------------------------------------- | 35 | // --------------------------------------------------------------------------- |
28 | 36 | ||
37 | const auditLogNameFilter = generateLogNameFilter(AUDIT_LOG_FILENAME) | ||
38 | async function getAuditLogs (req: express.Request, res: express.Response) { | ||
39 | const output = await generateOutput({ | ||
40 | startDateQuery: req.query.startDate, | ||
41 | endDateQuery: req.query.endDate, | ||
42 | level: 'audit', | ||
43 | nameFilter: auditLogNameFilter | ||
44 | }) | ||
45 | |||
46 | return res.json(output).end() | ||
47 | } | ||
48 | |||
49 | const logNameFilter = generateLogNameFilter(LOG_FILENAME) | ||
29 | async function getLogs (req: express.Request, res: express.Response) { | 50 | async function getLogs (req: express.Request, res: express.Response) { |
51 | const output = await generateOutput({ | ||
52 | startDateQuery: req.query.startDate, | ||
53 | endDateQuery: req.query.endDate, | ||
54 | level: req.query.level || 'info', | ||
55 | nameFilter: logNameFilter | ||
56 | }) | ||
57 | |||
58 | return res.json(output).end() | ||
59 | } | ||
60 | |||
61 | async function generateOutput (options: { | ||
62 | startDateQuery: string, | ||
63 | endDateQuery?: string, | ||
64 | level: LogLevel, | ||
65 | nameFilter: RegExp | ||
66 | }) { | ||
67 | const { startDateQuery, level, nameFilter } = options | ||
68 | |||
30 | const logFiles = await readdir(CONFIG.STORAGE.LOG_DIR) | 69 | const logFiles = await readdir(CONFIG.STORAGE.LOG_DIR) |
31 | const sortedLogFiles = await mtimeSortFilesDesc(logFiles, CONFIG.STORAGE.LOG_DIR) | 70 | const sortedLogFiles = await mtimeSortFilesDesc(logFiles, CONFIG.STORAGE.LOG_DIR) |
32 | let currentSize = 0 | 71 | let currentSize = 0 |
33 | 72 | ||
34 | const startDate = new Date(req.query.startDate) | 73 | const startDate = new Date(startDateQuery) |
35 | const endDate = req.query.endDate ? new Date(req.query.endDate) : new Date() | 74 | const endDate = options.endDateQuery ? new Date(options.endDateQuery) : new Date() |
36 | const level: LogLevel = req.query.level || 'info' | ||
37 | 75 | ||
38 | let output: string[] = [] | 76 | let output: string[] = [] |
39 | 77 | ||
40 | for (const meta of sortedLogFiles) { | 78 | for (const meta of sortedLogFiles) { |
79 | if (nameFilter.exec(meta.file) === null) continue | ||
80 | |||
41 | const path = join(CONFIG.STORAGE.LOG_DIR, meta.file) | 81 | const path = join(CONFIG.STORAGE.LOG_DIR, meta.file) |
42 | 82 | ||
43 | const result = await getOutputFromFile(path, startDate, endDate, level, currentSize) | 83 | const result = await getOutputFromFile(path, startDate, endDate, level, currentSize) |
@@ -49,7 +89,7 @@ async function getLogs (req: express.Request, res: express.Response) { | |||
49 | if (currentSize > MAX_LOGS_OUTPUT_CHARACTERS || (result.logTime && result.logTime < startDate.getTime())) break | 89 | if (currentSize > MAX_LOGS_OUTPUT_CHARACTERS || (result.logTime && result.logTime < startDate.getTime())) break |
50 | } | 90 | } |
51 | 91 | ||
52 | return res.json(output).end() | 92 | return output |
53 | } | 93 | } |
54 | 94 | ||
55 | async function getOutputFromFile (path: string, startDate: Date, endDate: Date, level: LogLevel, currentSize: number) { | 95 | async function getOutputFromFile (path: string, startDate: Date, endDate: Date, level: LogLevel, currentSize: number) { |
@@ -58,6 +98,7 @@ async function getOutputFromFile (path: string, startDate: Date, endDate: Date, | |||
58 | let logTime: number | 98 | let logTime: number |
59 | 99 | ||
60 | const logsLevel: { [ id in LogLevel ]: number } = { | 100 | const logsLevel: { [ id in LogLevel ]: number } = { |
101 | audit: -1, | ||
61 | debug: 0, | 102 | debug: 0, |
62 | info: 1, | 103 | info: 1, |
63 | warn: 2, | 104 | warn: 2, |
@@ -93,3 +134,7 @@ async function getOutputFromFile (path: string, startDate: Date, endDate: Date, | |||
93 | 134 | ||
94 | return { currentSize, output: output.reverse(), logTime } | 135 | return { currentSize, output: output.reverse(), logTime } |
95 | } | 136 | } |
137 | |||
138 | function generateLogNameFilter (baseName: string) { | ||
139 | return new RegExp('^' + baseName.replace(/\.log$/, '') + '\d*.log$') | ||
140 | } | ||
diff --git a/server/helpers/audit-logger.ts b/server/helpers/audit-logger.ts index f536da439..9b258dc3a 100644 --- a/server/helpers/audit-logger.ts +++ b/server/helpers/audit-logger.ts | |||
@@ -9,6 +9,7 @@ import { User, VideoAbuse, VideoChannel, VideoDetails, VideoImport } from '../.. | |||
9 | import { VideoComment } from '../../shared/models/videos/video-comment.model' | 9 | import { VideoComment } from '../../shared/models/videos/video-comment.model' |
10 | import { CustomConfig } from '../../shared/models/server/custom-config.model' | 10 | import { CustomConfig } from '../../shared/models/server/custom-config.model' |
11 | import { CONFIG } from '../initializers/config' | 11 | import { CONFIG } from '../initializers/config' |
12 | import { AUDIT_LOG_FILENAME } from '@server/initializers/constants' | ||
12 | 13 | ||
13 | function getAuditIdFromRes (res: express.Response) { | 14 | function getAuditIdFromRes (res: express.Response) { |
14 | return res.locals.oauth.token.User.username | 15 | return res.locals.oauth.token.User.username |
@@ -29,7 +30,7 @@ const auditLogger = winston.createLogger({ | |||
29 | levels: { audit: 0 }, | 30 | levels: { audit: 0 }, |
30 | transports: [ | 31 | transports: [ |
31 | new winston.transports.File({ | 32 | new winston.transports.File({ |
32 | filename: path.join(CONFIG.STORAGE.LOG_DIR, 'peertube-audit.log'), | 33 | filename: path.join(CONFIG.STORAGE.LOG_DIR, AUDIT_LOG_FILENAME), |
33 | level: 'audit', | 34 | level: 'audit', |
34 | maxsize: 5242880, | 35 | maxsize: 5242880, |
35 | maxFiles: 5, | 36 | maxFiles: 5, |
diff --git a/server/helpers/logger.ts b/server/helpers/logger.ts index d21746963..c2ff2bae6 100644 --- a/server/helpers/logger.ts +++ b/server/helpers/logger.ts | |||
@@ -5,6 +5,7 @@ import * as winston from 'winston' | |||
5 | import { FileTransportOptions } from 'winston/lib/winston/transports' | 5 | import { FileTransportOptions } from 'winston/lib/winston/transports' |
6 | import { CONFIG } from '../initializers/config' | 6 | import { CONFIG } from '../initializers/config' |
7 | import { omit } from 'lodash' | 7 | import { omit } from 'lodash' |
8 | import { LOG_FILENAME } from '@server/initializers/constants' | ||
8 | 9 | ||
9 | const label = CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT | 10 | const label = CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT |
10 | 11 | ||
@@ -58,7 +59,7 @@ const labelFormatter = winston.format.label({ | |||
58 | }) | 59 | }) |
59 | 60 | ||
60 | const fileLoggerOptions: FileTransportOptions = { | 61 | const fileLoggerOptions: FileTransportOptions = { |
61 | filename: path.join(CONFIG.STORAGE.LOG_DIR, 'peertube.log'), | 62 | filename: path.join(CONFIG.STORAGE.LOG_DIR, LOG_FILENAME), |
62 | handleExceptions: true, | 63 | handleExceptions: true, |
63 | format: winston.format.combine( | 64 | format: winston.format.combine( |
64 | winston.format.timestamp(), | 65 | winston.format.timestamp(), |
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts index af70e7b88..bdabe7f66 100644 --- a/server/initializers/constants.ts +++ b/server/initializers/constants.ts | |||
@@ -603,6 +603,8 @@ const FEEDS = { | |||
603 | } | 603 | } |
604 | 604 | ||
605 | const MAX_LOGS_OUTPUT_CHARACTERS = 10 * 1000 * 1000 | 605 | const MAX_LOGS_OUTPUT_CHARACTERS = 10 * 1000 * 1000 |
606 | const LOG_FILENAME = 'peertube.log' | ||
607 | const AUDIT_LOG_FILENAME = 'peertube-audit.log' | ||
606 | 608 | ||
607 | // --------------------------------------------------------------------------- | 609 | // --------------------------------------------------------------------------- |
608 | 610 | ||
@@ -684,6 +686,7 @@ export { | |||
684 | BCRYPT_SALT_SIZE, | 686 | BCRYPT_SALT_SIZE, |
685 | TRACKER_RATE_LIMITS, | 687 | TRACKER_RATE_LIMITS, |
686 | FILES_CACHE, | 688 | FILES_CACHE, |
689 | LOG_FILENAME, | ||
687 | CONSTRAINTS_FIELDS, | 690 | CONSTRAINTS_FIELDS, |
688 | EMBED_SIZE, | 691 | EMBED_SIZE, |
689 | REDUNDANCY, | 692 | REDUNDANCY, |
@@ -693,6 +696,7 @@ export { | |||
693 | OAUTH_LIFETIME, | 696 | OAUTH_LIFETIME, |
694 | CUSTOM_HTML_TAG_COMMENTS, | 697 | CUSTOM_HTML_TAG_COMMENTS, |
695 | BROADCAST_CONCURRENCY, | 698 | BROADCAST_CONCURRENCY, |
699 | AUDIT_LOG_FILENAME, | ||
696 | PAGINATION, | 700 | PAGINATION, |
697 | ACTOR_FOLLOW_SCORE, | 701 | ACTOR_FOLLOW_SCORE, |
698 | PREVIEWS_SIZE, | 702 | PREVIEWS_SIZE, |
diff --git a/server/middlewares/validators/logs.ts b/server/middlewares/validators/logs.ts index 07f3f552f..70e4d0d99 100644 --- a/server/middlewares/validators/logs.ts +++ b/server/middlewares/validators/logs.ts | |||
@@ -24,8 +24,25 @@ const getLogsValidator = [ | |||
24 | } | 24 | } |
25 | ] | 25 | ] |
26 | 26 | ||
27 | const getAuditLogsValidator = [ | ||
28 | query('startDate') | ||
29 | .custom(isDateValid).withMessage('Should have a valid start date'), | ||
30 | query('endDate') | ||
31 | .optional() | ||
32 | .custom(isDateValid).withMessage('Should have a valid end date'), | ||
33 | |||
34 | (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
35 | logger.debug('Checking getAuditLogsValidator parameters.', { parameters: req.query }) | ||
36 | |||
37 | if (areValidationErrors(req, res)) return | ||
38 | |||
39 | return next() | ||
40 | } | ||
41 | ] | ||
42 | |||
27 | // --------------------------------------------------------------------------- | 43 | // --------------------------------------------------------------------------- |
28 | 44 | ||
29 | export { | 45 | export { |
30 | getLogsValidator | 46 | getLogsValidator, |
47 | getAuditLogsValidator | ||
31 | } | 48 | } |
diff --git a/server/tests/api/server/logs.ts b/server/tests/api/server/logs.ts index 68f442199..d3c877408 100644 --- a/server/tests/api/server/logs.ts +++ b/server/tests/api/server/logs.ts | |||
@@ -2,17 +2,10 @@ | |||
2 | 2 | ||
3 | import * as chai from 'chai' | 3 | import * as chai from 'chai' |
4 | import 'mocha' | 4 | import 'mocha' |
5 | import { | 5 | import { cleanupTests, flushAndRunServer, ServerInfo, setAccessTokensToServers } from '../../../../shared/extra-utils/index' |
6 | flushTests, | ||
7 | killallServers, | ||
8 | flushAndRunServer, | ||
9 | ServerInfo, | ||
10 | setAccessTokensToServers, | ||
11 | cleanupTests | ||
12 | } from '../../../../shared/extra-utils/index' | ||
13 | import { waitJobs } from '../../../../shared/extra-utils/server/jobs' | 6 | import { waitJobs } from '../../../../shared/extra-utils/server/jobs' |
14 | import { uploadVideo } from '../../../../shared/extra-utils/videos/videos' | 7 | import { uploadVideo } from '../../../../shared/extra-utils/videos/videos' |
15 | import { getLogs } from '../../../../shared/extra-utils/logs/logs' | 8 | import { getAuditLogs, getLogs } from '../../../../shared/extra-utils/logs/logs' |
16 | 9 | ||
17 | const expect = chai.expect | 10 | const expect = chai.expect |
18 | 11 | ||
@@ -26,69 +19,123 @@ describe('Test logs', function () { | |||
26 | await setAccessTokensToServers([ server ]) | 19 | await setAccessTokensToServers([ server ]) |
27 | }) | 20 | }) |
28 | 21 | ||
29 | it('Should get logs with a start date', async function () { | 22 | describe('With the standard log file', function () { |
30 | this.timeout(10000) | 23 | it('Should get logs with a start date', async function () { |
24 | this.timeout(10000) | ||
31 | 25 | ||
32 | await uploadVideo(server.url, server.accessToken, { name: 'video 1' }) | 26 | await uploadVideo(server.url, server.accessToken, { name: 'video 1' }) |
33 | await waitJobs([ server ]) | 27 | await waitJobs([ server ]) |
34 | 28 | ||
35 | const now = new Date() | 29 | const now = new Date() |
36 | 30 | ||
37 | await uploadVideo(server.url, server.accessToken, { name: 'video 2' }) | 31 | await uploadVideo(server.url, server.accessToken, { name: 'video 2' }) |
38 | await waitJobs([ server ]) | 32 | await waitJobs([ server ]) |
39 | 33 | ||
40 | const res = await getLogs(server.url, server.accessToken, now) | 34 | const res = await getLogs(server.url, server.accessToken, now) |
41 | const logsString = JSON.stringify(res.body) | 35 | const logsString = JSON.stringify(res.body) |
42 | 36 | ||
43 | expect(logsString.includes('video 1')).to.be.false | 37 | expect(logsString.includes('video 1')).to.be.false |
44 | expect(logsString.includes('video 2')).to.be.true | 38 | expect(logsString.includes('video 2')).to.be.true |
45 | }) | 39 | }) |
40 | |||
41 | it('Should get logs with an end date', async function () { | ||
42 | this.timeout(20000) | ||
43 | |||
44 | await uploadVideo(server.url, server.accessToken, { name: 'video 3' }) | ||
45 | await waitJobs([ server ]) | ||
46 | |||
47 | const now1 = new Date() | ||
48 | |||
49 | await uploadVideo(server.url, server.accessToken, { name: 'video 4' }) | ||
50 | await waitJobs([ server ]) | ||
51 | |||
52 | const now2 = new Date() | ||
53 | |||
54 | await uploadVideo(server.url, server.accessToken, { name: 'video 5' }) | ||
55 | await waitJobs([ server ]) | ||
56 | |||
57 | const res = await getLogs(server.url, server.accessToken, now1, now2) | ||
58 | const logsString = JSON.stringify(res.body) | ||
46 | 59 | ||
47 | it('Should get logs with an end date', async function () { | 60 | expect(logsString.includes('video 3')).to.be.false |
48 | this.timeout(20000) | 61 | expect(logsString.includes('video 4')).to.be.true |
62 | expect(logsString.includes('video 5')).to.be.false | ||
63 | }) | ||
49 | 64 | ||
50 | await uploadVideo(server.url, server.accessToken, { name: 'video 3' }) | 65 | it('Should get filter by level', async function () { |
51 | await waitJobs([ server ]) | 66 | this.timeout(10000) |
52 | 67 | ||
53 | const now1 = new Date() | 68 | const now = new Date() |
54 | 69 | ||
55 | await uploadVideo(server.url, server.accessToken, { name: 'video 4' }) | 70 | await uploadVideo(server.url, server.accessToken, { name: 'video 6' }) |
56 | await waitJobs([ server ]) | 71 | await waitJobs([ server ]) |
57 | 72 | ||
58 | const now2 = new Date() | 73 | { |
74 | const res = await getLogs(server.url, server.accessToken, now, undefined, 'info') | ||
75 | const logsString = JSON.stringify(res.body) | ||
59 | 76 | ||
60 | await uploadVideo(server.url, server.accessToken, { name: 'video 5' }) | 77 | expect(logsString.includes('video 6')).to.be.true |
61 | await waitJobs([ server ]) | 78 | } |
62 | 79 | ||
63 | const res = await getLogs(server.url, server.accessToken, now1, now2) | 80 | { |
64 | const logsString = JSON.stringify(res.body) | 81 | const res = await getLogs(server.url, server.accessToken, now, undefined, 'warn') |
82 | const logsString = JSON.stringify(res.body) | ||
65 | 83 | ||
66 | expect(logsString.includes('video 3')).to.be.false | 84 | expect(logsString.includes('video 6')).to.be.false |
67 | expect(logsString.includes('video 4')).to.be.true | 85 | } |
68 | expect(logsString.includes('video 5')).to.be.false | 86 | }) |
69 | }) | 87 | }) |
70 | 88 | ||
71 | it('Should get filter by level', async function () { | 89 | describe('With the audit log', function () { |
72 | this.timeout(10000) | 90 | it('Should get logs with a start date', async function () { |
91 | this.timeout(10000) | ||
73 | 92 | ||
74 | const now = new Date() | 93 | await uploadVideo(server.url, server.accessToken, { name: 'video 7' }) |
94 | await waitJobs([ server ]) | ||
75 | 95 | ||
76 | await uploadVideo(server.url, server.accessToken, { name: 'video 6' }) | 96 | const now = new Date() |
77 | await waitJobs([ server ]) | ||
78 | 97 | ||
79 | { | 98 | await uploadVideo(server.url, server.accessToken, { name: 'video 8' }) |
80 | const res = await getLogs(server.url, server.accessToken, now, undefined, 'info') | 99 | await waitJobs([ server ]) |
100 | |||
101 | const res = await getAuditLogs(server.url, server.accessToken, now) | ||
81 | const logsString = JSON.stringify(res.body) | 102 | const logsString = JSON.stringify(res.body) |
82 | 103 | ||
83 | expect(logsString.includes('video 6')).to.be.true | 104 | expect(logsString.includes('video 7')).to.be.false |
84 | } | 105 | expect(logsString.includes('video 8')).to.be.true |
106 | |||
107 | expect(res.body).to.have.lengthOf(1) | ||
108 | |||
109 | const item = res.body[0] | ||
110 | |||
111 | const message = JSON.parse(item.message) | ||
112 | expect(message.domain).to.equal('videos') | ||
113 | expect(message.action).to.equal('create') | ||
114 | }) | ||
115 | |||
116 | it('Should get logs with an end date', async function () { | ||
117 | this.timeout(20000) | ||
118 | |||
119 | await uploadVideo(server.url, server.accessToken, { name: 'video 9' }) | ||
120 | await waitJobs([ server ]) | ||
121 | |||
122 | const now1 = new Date() | ||
123 | |||
124 | await uploadVideo(server.url, server.accessToken, { name: 'video 10' }) | ||
125 | await waitJobs([ server ]) | ||
126 | |||
127 | const now2 = new Date() | ||
128 | |||
129 | await uploadVideo(server.url, server.accessToken, { name: 'video 11' }) | ||
130 | await waitJobs([ server ]) | ||
85 | 131 | ||
86 | { | 132 | const res = await getAuditLogs(server.url, server.accessToken, now1, now2) |
87 | const res = await getLogs(server.url, server.accessToken, now, undefined, 'warn') | ||
88 | const logsString = JSON.stringify(res.body) | 133 | const logsString = JSON.stringify(res.body) |
89 | 134 | ||
90 | expect(logsString.includes('video 6')).to.be.false | 135 | expect(logsString.includes('video 9')).to.be.false |
91 | } | 136 | expect(logsString.includes('video 10')).to.be.true |
137 | expect(logsString.includes('video 11')).to.be.false | ||
138 | }) | ||
92 | }) | 139 | }) |
93 | 140 | ||
94 | after(async function () { | 141 | after(async function () { |
diff --git a/shared/extra-utils/logs/logs.ts b/shared/extra-utils/logs/logs.ts index cbb1afb93..c494c1f1e 100644 --- a/shared/extra-utils/logs/logs.ts +++ b/shared/extra-utils/logs/logs.ts | |||
@@ -13,6 +13,19 @@ function getLogs (url: string, accessToken: string, startDate: Date, endDate?: D | |||
13 | }) | 13 | }) |
14 | } | 14 | } |
15 | 15 | ||
16 | function getAuditLogs (url: string, accessToken: string, startDate: Date, endDate?: Date) { | ||
17 | const path = '/api/v1/server/audit-logs' | ||
18 | |||
19 | return makeGetRequest({ | ||
20 | url, | ||
21 | path, | ||
22 | token: accessToken, | ||
23 | query: { startDate, endDate }, | ||
24 | statusCodeExpected: 200 | ||
25 | }) | ||
26 | } | ||
27 | |||
16 | export { | 28 | export { |
17 | getLogs | 29 | getLogs, |
30 | getAuditLogs | ||
18 | } | 31 | } |
diff --git a/shared/models/server/log-level.type.ts b/shared/models/server/log-level.type.ts index ce91559e3..4afb92d11 100644 --- a/shared/models/server/log-level.type.ts +++ b/shared/models/server/log-level.type.ts | |||
@@ -1 +1 @@ | |||
export type LogLevel = 'debug' | 'info' | 'warn' | 'error' | export type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'audit' | ||