aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--client/src/app/+admin/system/logs/logs.component.html2
-rw-r--r--client/src/app/+admin/system/logs/logs.component.scss4
-rw-r--r--client/src/app/+admin/system/logs/logs.component.ts33
-rw-r--r--client/src/app/+admin/system/logs/logs.service.ts9
-rw-r--r--client/src/app/shared/shared-forms/select/select-tags.component.html2
-rw-r--r--client/src/app/shared/shared-forms/select/select-tags.component.ts1
-rw-r--r--server/controllers/api/server/logs.ts42
-rw-r--r--server/middlewares/validators/logs.ts7
-rw-r--r--server/tests/api/server/logs.ts23
-rw-r--r--shared/extra-utils/logs/logs-command.ts7
10 files changed, 102 insertions, 28 deletions
diff --git a/client/src/app/+admin/system/logs/logs.component.html b/client/src/app/+admin/system/logs/logs.component.html
index b2c7f84bc..18011b205 100644
--- a/client/src/app/+admin/system/logs/logs.component.html
+++ b/client/src/app/+admin/system/logs/logs.component.html
@@ -28,6 +28,8 @@
28 </ng-option> 28 </ng-option>
29 </ng-select> 29 </ng-select>
30 30
31 <my-select-tags i18n-placeholder placeholder="Filter logs by tags" [(ngModel)]="tagsOneOf" (ngModelChange)="refresh()"></my-select-tags>
32
31 <my-button i18n-label label="Refresh" icon="refresh" (click)="refresh()"></my-button> 33 <my-button i18n-label label="Refresh" icon="refresh" (click)="refresh()"></my-button>
32</div> 34</div>
33 35
diff --git a/client/src/app/+admin/system/logs/logs.component.scss b/client/src/app/+admin/system/logs/logs.component.scss
index fefa7efc2..be66d563b 100644
--- a/client/src/app/+admin/system/logs/logs.component.scss
+++ b/client/src/app/+admin/system/logs/logs.component.scss
@@ -52,9 +52,7 @@
52 @include peertube-select-container(150px); 52 @include peertube-select-container(150px);
53 } 53 }
54 54
55 my-button, 55 > * {
56 .peertube-select-container,
57 ng-select {
58 @include margin-left(10px); 56 @include margin-left(10px);
59 } 57 }
60} 58}
diff --git a/client/src/app/+admin/system/logs/logs.component.ts b/client/src/app/+admin/system/logs/logs.component.ts
index 865ab80a2..06237522a 100644
--- a/client/src/app/+admin/system/logs/logs.component.ts
+++ b/client/src/app/+admin/system/logs/logs.component.ts
@@ -23,6 +23,7 @@ export class LogsComponent implements OnInit {
23 startDate: string 23 startDate: string
24 level: LogLevel 24 level: LogLevel
25 logType: 'audit' | 'standard' 25 logType: 'audit' | 'standard'
26 tagsOneOf: string[] = []
26 27
27 constructor ( 28 constructor (
28 private logsService: LogsService, 29 private logsService: LogsService,
@@ -51,20 +52,28 @@ export class LogsComponent implements OnInit {
51 load () { 52 load () {
52 this.loading = true 53 this.loading = true
53 54
54 this.logsService.getLogs({ isAuditLog: this.isAuditLog(), level: this.level, startDate: this.startDate }) 55 const tagsOneOf = this.tagsOneOf.length !== 0
55 .subscribe({ 56 ? this.tagsOneOf
56 next: logs => { 57 : undefined
57 this.logs = logs 58
58 59 this.logsService.getLogs({
59 setTimeout(() => { 60 isAuditLog: this.isAuditLog(),
60 this.logsElement.nativeElement.scrollIntoView({ block: 'end', inline: 'nearest' }) 61 level: this.level,
61 }) 62 startDate: this.startDate,
62 }, 63 tagsOneOf
64 }).subscribe({
65 next: logs => {
66 this.logs = logs
67
68 setTimeout(() => {
69 this.logsElement.nativeElement.scrollIntoView({ block: 'end', inline: 'nearest' })
70 })
71 },
63 72
64 error: err => this.notifier.error(err.message), 73 error: err => this.notifier.error(err.message),
65 74
66 complete: () => this.loading = false 75 complete: () => this.loading = false
67 }) 76 })
68 } 77 }
69 78
70 isAuditLog () { 79 isAuditLog () {
diff --git a/client/src/app/+admin/system/logs/logs.service.ts b/client/src/app/+admin/system/logs/logs.service.ts
index 0c222cad2..ea7e08b9b 100644
--- a/client/src/app/+admin/system/logs/logs.service.ts
+++ b/client/src/app/+admin/system/logs/logs.service.ts
@@ -2,7 +2,7 @@ import { Observable } from 'rxjs'
2import { catchError, map } from 'rxjs/operators' 2import { catchError, map } from 'rxjs/operators'
3import { HttpClient, HttpParams } from '@angular/common/http' 3import { HttpClient, HttpParams } from '@angular/common/http'
4import { Injectable } from '@angular/core' 4import { Injectable } from '@angular/core'
5import { RestExtractor } from '@app/core' 5import { RestExtractor, RestService } from '@app/core'
6import { LogLevel } from '@shared/models' 6import { LogLevel } from '@shared/models'
7import { environment } from '../../../../environments/environment' 7import { environment } from '../../../../environments/environment'
8import { LogRow } from './log-row.model' 8import { LogRow } from './log-row.model'
@@ -14,22 +14,25 @@ export class LogsService {
14 14
15 constructor ( 15 constructor (
16 private authHttp: HttpClient, 16 private authHttp: HttpClient,
17 private restService: RestService,
17 private restExtractor: RestExtractor 18 private restExtractor: RestExtractor
18 ) {} 19 ) {}
19 20
20 getLogs (options: { 21 getLogs (options: {
21 isAuditLog: boolean 22 isAuditLog: boolean
22 startDate: string 23 startDate: string
24 tagsOneOf?: string[]
23 level?: LogLevel 25 level?: LogLevel
24 endDate?: string 26 endDate?: string
25 }): Observable<any[]> { 27 }): Observable<any[]> {
26 const { isAuditLog, startDate } = options 28 const { isAuditLog, startDate, endDate, tagsOneOf } = options
27 29
28 let params = new HttpParams() 30 let params = new HttpParams()
29 params = params.append('startDate', startDate) 31 params = params.append('startDate', startDate)
30 32
31 if (!isAuditLog) params = params.append('level', options.level) 33 if (!isAuditLog) params = params.append('level', options.level)
32 if (options.endDate) params.append('endDate', options.endDate) 34 if (endDate) params = params.append('endDate', options.endDate)
35 if (tagsOneOf) params = this.restService.addArrayParams(params, 'tagsOneOf', tagsOneOf)
33 36
34 const path = isAuditLog 37 const path = isAuditLog
35 ? LogsService.BASE_AUDIT_LOG_URL 38 ? LogsService.BASE_AUDIT_LOG_URL
diff --git a/client/src/app/shared/shared-forms/select/select-tags.component.html b/client/src/app/shared/shared-forms/select/select-tags.component.html
index e1cd50882..de6cee6db 100644
--- a/client/src/app/shared/shared-forms/select/select-tags.component.html
+++ b/client/src/app/shared/shared-forms/select/select-tags.component.html
@@ -2,7 +2,7 @@
2 [items]="availableItems" 2 [items]="availableItems"
3 [(ngModel)]="selectedItems" 3 [(ngModel)]="selectedItems"
4 (ngModelChange)="onModelChange()" 4 (ngModelChange)="onModelChange()"
5 i18n-placeholder placeholder="Enter a new tag" 5 [placeholder]="placeholder"
6 [maxSelectedItems]="5" 6 [maxSelectedItems]="5"
7 [clearable]="true" 7 [clearable]="true"
8 [addTag]="true" 8 [addTag]="true"
diff --git a/client/src/app/shared/shared-forms/select/select-tags.component.ts b/client/src/app/shared/shared-forms/select/select-tags.component.ts
index 93d199037..bef04de8a 100644
--- a/client/src/app/shared/shared-forms/select/select-tags.component.ts
+++ b/client/src/app/shared/shared-forms/select/select-tags.component.ts
@@ -16,6 +16,7 @@ import { NG_VALUE_ACCESSOR, ControlValueAccessor } from '@angular/forms'
16export class SelectTagsComponent implements ControlValueAccessor { 16export class SelectTagsComponent implements ControlValueAccessor {
17 @Input() availableItems: string[] = [] 17 @Input() availableItems: string[] = []
18 @Input() selectedItems: string[] = [] 18 @Input() selectedItems: string[] = []
19 @Input() placeholder = $localize`Enter a new tag`
19 20
20 propagateChange = (_: any) => { /* empty */ } 21 propagateChange = (_: any) => { /* empty */ }
21 22
diff --git a/server/controllers/api/server/logs.ts b/server/controllers/api/server/logs.ts
index dfd5491aa..8aa4b7190 100644
--- a/server/controllers/api/server/logs.ts
+++ b/server/controllers/api/server/logs.ts
@@ -1,6 +1,7 @@
1import express from 'express' 1import express from 'express'
2import { readdir, readFile } from 'fs-extra' 2import { readdir, readFile } from 'fs-extra'
3import { join } from 'path' 3import { join } from 'path'
4import { isArray } from '@server/helpers/custom-validators/misc'
4import { logger, mtimeSortFilesDesc } from '@server/helpers/logger' 5import { logger, mtimeSortFilesDesc } from '@server/helpers/logger'
5import { LogLevel } from '../../../../shared/models/server/log-level.type' 6import { LogLevel } from '../../../../shared/models/server/log-level.type'
6import { UserRight } from '../../../../shared/models/users' 7import { UserRight } from '../../../../shared/models/users'
@@ -51,20 +52,27 @@ async function getLogs (req: express.Request, res: express.Response) {
51 startDateQuery: req.query.startDate, 52 startDateQuery: req.query.startDate,
52 endDateQuery: req.query.endDate, 53 endDateQuery: req.query.endDate,
53 level: req.query.level || 'info', 54 level: req.query.level || 'info',
55 tagsOneOf: req.query.tagsOneOf,
54 nameFilter: logNameFilter 56 nameFilter: logNameFilter
55 }) 57 })
56 58
57 return res.json(output).end() 59 return res.json(output)
58} 60}
59 61
60async function generateOutput (options: { 62async function generateOutput (options: {
61 startDateQuery: string 63 startDateQuery: string
62 endDateQuery?: string 64 endDateQuery?: string
65
63 level: LogLevel 66 level: LogLevel
64 nameFilter: RegExp 67 nameFilter: RegExp
68 tagsOneOf?: string[]
65}) { 69}) {
66 const { startDateQuery, level, nameFilter } = options 70 const { startDateQuery, level, nameFilter } = options
67 71
72 const tagsOneOf = Array.isArray(options.tagsOneOf) && options.tagsOneOf.length !== 0
73 ? new Set(options.tagsOneOf)
74 : undefined
75
68 const logFiles = await readdir(CONFIG.STORAGE.LOG_DIR) 76 const logFiles = await readdir(CONFIG.STORAGE.LOG_DIR)
69 const sortedLogFiles = await mtimeSortFilesDesc(logFiles, CONFIG.STORAGE.LOG_DIR) 77 const sortedLogFiles = await mtimeSortFilesDesc(logFiles, CONFIG.STORAGE.LOG_DIR)
70 let currentSize = 0 78 let currentSize = 0
@@ -80,7 +88,7 @@ async function generateOutput (options: {
80 const path = join(CONFIG.STORAGE.LOG_DIR, meta.file) 88 const path = join(CONFIG.STORAGE.LOG_DIR, meta.file)
81 logger.debug('Opening %s to fetch logs.', path) 89 logger.debug('Opening %s to fetch logs.', path)
82 90
83 const result = await getOutputFromFile(path, startDate, endDate, level, currentSize) 91 const result = await getOutputFromFile({ path, startDate, endDate, level, currentSize, tagsOneOf })
84 if (!result.output) break 92 if (!result.output) break
85 93
86 output = result.output.concat(output) 94 output = result.output.concat(output)
@@ -92,9 +100,20 @@ async function generateOutput (options: {
92 return output 100 return output
93} 101}
94 102
95async function getOutputFromFile (path: string, startDate: Date, endDate: Date, level: LogLevel, currentSize: number) { 103async function getOutputFromFile (options: {
104 path: string
105 startDate: Date
106 endDate: Date
107 level: LogLevel
108 currentSize: number
109 tagsOneOf: Set<string>
110}) {
111 const { path, startDate, endDate, level, tagsOneOf } = options
112
96 const startTime = startDate.getTime() 113 const startTime = startDate.getTime()
97 const endTime = endDate.getTime() 114 const endTime = endDate.getTime()
115 let currentSize = options.currentSize
116
98 let logTime: number 117 let logTime: number
99 118
100 const logsLevel: { [ id in LogLevel ]: number } = { 119 const logsLevel: { [ id in LogLevel ]: number } = {
@@ -121,7 +140,12 @@ async function getOutputFromFile (path: string, startDate: Date, endDate: Date,
121 } 140 }
122 141
123 logTime = new Date(log.timestamp).getTime() 142 logTime = new Date(log.timestamp).getTime()
124 if (logTime >= startTime && logTime <= endTime && logsLevel[log.level] >= logsLevel[level]) { 143 if (
144 logTime >= startTime &&
145 logTime <= endTime &&
146 logsLevel[log.level] >= logsLevel[level] &&
147 (!tagsOneOf || lineHasTag(log, tagsOneOf))
148 ) {
125 output.push(log) 149 output.push(log)
126 150
127 currentSize += line.length 151 currentSize += line.length
@@ -135,6 +159,16 @@ async function getOutputFromFile (path: string, startDate: Date, endDate: Date,
135 return { currentSize, output: output.reverse(), logTime } 159 return { currentSize, output: output.reverse(), logTime }
136} 160}
137 161
162function lineHasTag (line: { tags?: string }, tagsOneOf: Set<string>) {
163 if (!isArray(line.tags)) return false
164
165 for (const lineTag of line.tags) {
166 if (tagsOneOf.has(lineTag)) return true
167 }
168
169 return false
170}
171
138function generateLogNameFilter (baseName: string) { 172function generateLogNameFilter (baseName: string) {
139 return new RegExp('^' + baseName.replace(/\.log$/, '') + '\\d*.log$') 173 return new RegExp('^' + baseName.replace(/\.log$/, '') + '\\d*.log$')
140} 174}
diff --git a/server/middlewares/validators/logs.ts b/server/middlewares/validators/logs.ts
index 03c1c4df1..901d8ca64 100644
--- a/server/middlewares/validators/logs.ts
+++ b/server/middlewares/validators/logs.ts
@@ -1,7 +1,8 @@
1import express from 'express' 1import express from 'express'
2import { query } from 'express-validator' 2import { query } from 'express-validator'
3import { isStringArray } from '@server/helpers/custom-validators/search'
3import { isValidLogLevel } from '../../helpers/custom-validators/logs' 4import { isValidLogLevel } from '../../helpers/custom-validators/logs'
4import { isDateValid } from '../../helpers/custom-validators/misc' 5import { isDateValid, toArray } from '../../helpers/custom-validators/misc'
5import { logger } from '../../helpers/logger' 6import { logger } from '../../helpers/logger'
6import { areValidationErrors } from './shared' 7import { areValidationErrors } from './shared'
7 8
@@ -11,6 +12,10 @@ const getLogsValidator = [
11 query('level') 12 query('level')
12 .optional() 13 .optional()
13 .custom(isValidLogLevel).withMessage('Should have a valid level'), 14 .custom(isValidLogLevel).withMessage('Should have a valid level'),
15 query('tagsOneOf')
16 .optional()
17 .customSanitizer(toArray)
18 .custom(isStringArray).withMessage('Should have a valid one of tags array'),
14 query('endDate') 19 query('endDate')
15 .optional() 20 .optional()
16 .custom(isDateValid).withMessage('Should have an end date that conforms to ISO 8601'), 21 .custom(isDateValid).withMessage('Should have an end date that conforms to ISO 8601'),
diff --git a/server/tests/api/server/logs.ts b/server/tests/api/server/logs.ts
index bcd94dda3..4fa13886e 100644
--- a/server/tests/api/server/logs.ts
+++ b/server/tests/api/server/logs.ts
@@ -71,7 +71,7 @@ describe('Test logs', function () {
71 expect(logsString.includes('video 5')).to.be.false 71 expect(logsString.includes('video 5')).to.be.false
72 }) 72 })
73 73
74 it('Should get filter by level', async function () { 74 it('Should filter by level', async function () {
75 this.timeout(20000) 75 this.timeout(20000)
76 76
77 const now = new Date() 77 const now = new Date()
@@ -94,6 +94,27 @@ describe('Test logs', function () {
94 } 94 }
95 }) 95 })
96 96
97 it('Should filter by tag', async function () {
98 const now = new Date()
99
100 const { uuid } = await server.videos.upload({ attributes: { name: 'video 6' } })
101 await waitJobs([ server ])
102
103 {
104 const body = await logsCommand.getLogs({ startDate: now, level: 'debug', tagsOneOf: [ 'toto' ] })
105 expect(body).to.have.lengthOf(0)
106 }
107
108 {
109 const body = await logsCommand.getLogs({ startDate: now, level: 'debug', tagsOneOf: [ uuid ] })
110 expect(body).to.not.have.lengthOf(0)
111
112 for (const line of body) {
113 expect(line.tags).to.contain(uuid)
114 }
115 }
116 })
117
97 it('Should log ping requests', async function () { 118 it('Should log ping requests', async function () {
98 this.timeout(10000) 119 this.timeout(10000)
99 120
diff --git a/shared/extra-utils/logs/logs-command.ts b/shared/extra-utils/logs/logs-command.ts
index 5912e814f..7b5c66c0c 100644
--- a/shared/extra-utils/logs/logs-command.ts
+++ b/shared/extra-utils/logs/logs-command.ts
@@ -8,15 +8,16 @@ export class LogsCommand extends AbstractCommand {
8 startDate: Date 8 startDate: Date
9 endDate?: Date 9 endDate?: Date
10 level?: LogLevel 10 level?: LogLevel
11 tagsOneOf?: string[]
11 }) { 12 }) {
12 const { startDate, endDate, level } = options 13 const { startDate, endDate, tagsOneOf, level } = options
13 const path = '/api/v1/server/logs' 14 const path = '/api/v1/server/logs'
14 15
15 return this.getRequestBody({ 16 return this.getRequestBody<any[]>({
16 ...options, 17 ...options,
17 18
18 path, 19 path,
19 query: { startDate, endDate, level }, 20 query: { startDate, endDate, level, tagsOneOf },
20 implicitToken: true, 21 implicitToken: true,
21 defaultExpectedStatus: HttpStatusCode.OK_200 22 defaultExpectedStatus: HttpStatusCode.OK_200
22 }) 23 })