aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.html2
-rw-r--r--client/src/app/+admin/moderation/video-blacklist-list/video-blacklist-list.component.html2
-rw-r--r--client/src/app/+admin/system/jobs/jobs.component.html2
-rw-r--r--client/src/app/+admin/system/logs/log-row.model.ts19
-rw-r--r--client/src/app/+admin/system/logs/logs.component.html13
-rw-r--r--client/src/app/+admin/system/logs/logs.component.scss10
-rw-r--r--client/src/app/+admin/system/logs/logs.component.ts24
-rw-r--r--client/src/app/+admin/system/logs/logs.service.ts20
-rw-r--r--client/src/app/+admin/users/user-list/user-list.component.html2
-rw-r--r--client/src/sass/primeng-custom.scss2
-rw-r--r--server/controllers/api/server/logs.ts57
-rw-r--r--server/helpers/audit-logger.ts3
-rw-r--r--server/helpers/logger.ts3
-rw-r--r--server/initializers/constants.ts4
-rw-r--r--server/middlewares/validators/logs.ts19
-rw-r--r--server/tests/api/server/logs.ts145
-rw-r--r--shared/extra-utils/logs/logs.ts15
-rw-r--r--shared/models/server/log-level.type.ts2
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()
11export class LogsService { 11export 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'
3import { asyncMiddleware, authenticate, ensureUserHasRight } from '../../../middlewares' 3import { asyncMiddleware, authenticate, ensureUserHasRight } from '../../../middlewares'
4import { mtimeSortFilesDesc } from '../../../../shared/core-utils/logs/logs' 4import { mtimeSortFilesDesc } from '../../../../shared/core-utils/logs/logs'
5import { readdir, readFile } from 'fs-extra' 5import { readdir, readFile } from 'fs-extra'
6import { MAX_LOGS_OUTPUT_CHARACTERS } from '../../../initializers/constants' 6import { AUDIT_LOG_FILENAME, MAX_LOGS_OUTPUT_CHARACTERS, LOG_FILENAME } from '../../../initializers/constants'
7import { join } from 'path' 7import { join } from 'path'
8import { getLogsValidator } from '../../../middlewares/validators/logs' 8import { getAuditLogsValidator, getLogsValidator } from '../../../middlewares/validators/logs'
9import { LogLevel } from '../../../../shared/models/server/log-level.type' 9import { LogLevel } from '../../../../shared/models/server/log-level.type'
10import { CONFIG } from '../../../initializers/config' 10import { CONFIG } from '../../../initializers/config'
11import { logger } from '@server/helpers/logger'
11 12
12const logsRouter = express.Router() 13const logsRouter = express.Router()
13 14
@@ -18,6 +19,13 @@ logsRouter.get('/logs',
18 asyncMiddleware(getLogs) 19 asyncMiddleware(getLogs)
19) 20)
20 21
22logsRouter.get('/audit-logs',
23 authenticate,
24 ensureUserHasRight(UserRight.MANAGE_LOGS),
25 getAuditLogsValidator,
26 asyncMiddleware(getAuditLogs)
27)
28
21// --------------------------------------------------------------------------- 29// ---------------------------------------------------------------------------
22 30
23export { 31export {
@@ -26,18 +34,50 @@ export {
26 34
27// --------------------------------------------------------------------------- 35// ---------------------------------------------------------------------------
28 36
37const auditLogNameFilter = generateLogNameFilter(AUDIT_LOG_FILENAME)
38async 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
49const logNameFilter = generateLogNameFilter(LOG_FILENAME)
29async function getLogs (req: express.Request, res: express.Response) { 50async 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
61async 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
55async function getOutputFromFile (path: string, startDate: Date, endDate: Date, level: LogLevel, currentSize: number) { 95async 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
138function 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 '../..
9import { VideoComment } from '../../shared/models/videos/video-comment.model' 9import { VideoComment } from '../../shared/models/videos/video-comment.model'
10import { CustomConfig } from '../../shared/models/server/custom-config.model' 10import { CustomConfig } from '../../shared/models/server/custom-config.model'
11import { CONFIG } from '../initializers/config' 11import { CONFIG } from '../initializers/config'
12import { AUDIT_LOG_FILENAME } from '@server/initializers/constants'
12 13
13function getAuditIdFromRes (res: express.Response) { 14function 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'
5import { FileTransportOptions } from 'winston/lib/winston/transports' 5import { FileTransportOptions } from 'winston/lib/winston/transports'
6import { CONFIG } from '../initializers/config' 6import { CONFIG } from '../initializers/config'
7import { omit } from 'lodash' 7import { omit } from 'lodash'
8import { LOG_FILENAME } from '@server/initializers/constants'
8 9
9const label = CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT 10const label = CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT
10 11
@@ -58,7 +59,7 @@ const labelFormatter = winston.format.label({
58}) 59})
59 60
60const fileLoggerOptions: FileTransportOptions = { 61const 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
605const MAX_LOGS_OUTPUT_CHARACTERS = 10 * 1000 * 1000 605const MAX_LOGS_OUTPUT_CHARACTERS = 10 * 1000 * 1000
606const LOG_FILENAME = 'peertube.log'
607const 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
27const 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
29export { 45export {
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
3import * as chai from 'chai' 3import * as chai from 'chai'
4import 'mocha' 4import 'mocha'
5import { 5import { 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'
13import { waitJobs } from '../../../../shared/extra-utils/server/jobs' 6import { waitJobs } from '../../../../shared/extra-utils/server/jobs'
14import { uploadVideo } from '../../../../shared/extra-utils/videos/videos' 7import { uploadVideo } from '../../../../shared/extra-utils/videos/videos'
15import { getLogs } from '../../../../shared/extra-utils/logs/logs' 8import { getAuditLogs, getLogs } from '../../../../shared/extra-utils/logs/logs'
16 9
17const expect = chai.expect 10const 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
16function 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
16export { 28export {
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'