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 @@
+
+
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 @@
@include peertube-select-container(150px);
}
- my-button,
- .peertube-select-container,
- ng-select {
+ > * {
@include margin-left(10px);
}
}
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 {
startDate: string
level: LogLevel
logType: 'audit' | 'standard'
+ tagsOneOf: string[] = []
constructor (
private logsService: LogsService,
@@ -51,20 +52,28 @@ export class LogsComponent implements OnInit {
load () {
this.loading = true
- this.logsService.getLogs({ isAuditLog: this.isAuditLog(), level: this.level, startDate: this.startDate })
- .subscribe({
- next: logs => {
- this.logs = logs
+ const tagsOneOf = this.tagsOneOf.length !== 0
+ ? this.tagsOneOf
+ : undefined
- setTimeout(() => {
- this.logsElement.nativeElement.scrollIntoView({ block: 'end', inline: 'nearest' })
- })
- },
+ this.logsService.getLogs({
+ isAuditLog: this.isAuditLog(),
+ level: this.level,
+ startDate: this.startDate,
+ tagsOneOf
+ }).subscribe({
+ next: logs => {
+ this.logs = logs
- error: err => this.notifier.error(err.message),
-
- complete: () => this.loading = false
+ setTimeout(() => {
+ this.logsElement.nativeElement.scrollIntoView({ block: 'end', inline: 'nearest' })
})
+ },
+
+ error: err => this.notifier.error(err.message),
+
+ complete: () => this.loading = false
+ })
}
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'
import { catchError, map } from 'rxjs/operators'
import { HttpClient, HttpParams } from '@angular/common/http'
import { Injectable } from '@angular/core'
-import { RestExtractor } from '@app/core'
+import { RestExtractor, RestService } from '@app/core'
import { LogLevel } from '@shared/models'
import { environment } from '../../../../environments/environment'
import { LogRow } from './log-row.model'
@@ -14,22 +14,25 @@ export class LogsService {
constructor (
private authHttp: HttpClient,
+ private restService: RestService,
private restExtractor: RestExtractor
) {}
getLogs (options: {
isAuditLog: boolean
startDate: string
+ tagsOneOf?: string[]
level?: LogLevel
endDate?: string
}): Observable {
- const { isAuditLog, startDate } = options
+ const { isAuditLog, startDate, endDate, tagsOneOf } = options
let params = new HttpParams()
params = params.append('startDate', startDate)
if (!isAuditLog) params = params.append('level', options.level)
- if (options.endDate) params.append('endDate', options.endDate)
+ if (endDate) params = params.append('endDate', options.endDate)
+ if (tagsOneOf) params = this.restService.addArrayParams(params, 'tagsOneOf', tagsOneOf)
const path = isAuditLog
? 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 @@
[items]="availableItems"
[(ngModel)]="selectedItems"
(ngModelChange)="onModelChange()"
- i18n-placeholder placeholder="Enter a new tag"
+ [placeholder]="placeholder"
[maxSelectedItems]="5"
[clearable]="true"
[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'
export class SelectTagsComponent implements ControlValueAccessor {
@Input() availableItems: string[] = []
@Input() selectedItems: string[] = []
+ @Input() placeholder = $localize`Enter a new tag`
propagateChange = (_: any) => { /* empty */ }
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 @@
import express from 'express'
import { readdir, readFile } from 'fs-extra'
import { join } from 'path'
+import { isArray } from '@server/helpers/custom-validators/misc'
import { logger, mtimeSortFilesDesc } from '@server/helpers/logger'
import { LogLevel } from '../../../../shared/models/server/log-level.type'
import { UserRight } from '../../../../shared/models/users'
@@ -51,20 +52,27 @@ async function getLogs (req: express.Request, res: express.Response) {
startDateQuery: req.query.startDate,
endDateQuery: req.query.endDate,
level: req.query.level || 'info',
+ tagsOneOf: req.query.tagsOneOf,
nameFilter: logNameFilter
})
- return res.json(output).end()
+ return res.json(output)
}
async function generateOutput (options: {
startDateQuery: string
endDateQuery?: string
+
level: LogLevel
nameFilter: RegExp
+ tagsOneOf?: string[]
}) {
const { startDateQuery, level, nameFilter } = options
+ const tagsOneOf = Array.isArray(options.tagsOneOf) && options.tagsOneOf.length !== 0
+ ? new Set(options.tagsOneOf)
+ : undefined
+
const logFiles = await readdir(CONFIG.STORAGE.LOG_DIR)
const sortedLogFiles = await mtimeSortFilesDesc(logFiles, CONFIG.STORAGE.LOG_DIR)
let currentSize = 0
@@ -80,7 +88,7 @@ async function generateOutput (options: {
const path = join(CONFIG.STORAGE.LOG_DIR, meta.file)
logger.debug('Opening %s to fetch logs.', path)
- const result = await getOutputFromFile(path, startDate, endDate, level, currentSize)
+ const result = await getOutputFromFile({ path, startDate, endDate, level, currentSize, tagsOneOf })
if (!result.output) break
output = result.output.concat(output)
@@ -92,9 +100,20 @@ async function generateOutput (options: {
return output
}
-async function getOutputFromFile (path: string, startDate: Date, endDate: Date, level: LogLevel, currentSize: number) {
+async function getOutputFromFile (options: {
+ path: string
+ startDate: Date
+ endDate: Date
+ level: LogLevel
+ currentSize: number
+ tagsOneOf: Set
+}) {
+ const { path, startDate, endDate, level, tagsOneOf } = options
+
const startTime = startDate.getTime()
const endTime = endDate.getTime()
+ let currentSize = options.currentSize
+
let logTime: number
const logsLevel: { [ id in LogLevel ]: number } = {
@@ -121,7 +140,12 @@ async function getOutputFromFile (path: string, startDate: Date, endDate: Date,
}
logTime = new Date(log.timestamp).getTime()
- if (logTime >= startTime && logTime <= endTime && logsLevel[log.level] >= logsLevel[level]) {
+ if (
+ logTime >= startTime &&
+ logTime <= endTime &&
+ logsLevel[log.level] >= logsLevel[level] &&
+ (!tagsOneOf || lineHasTag(log, tagsOneOf))
+ ) {
output.push(log)
currentSize += line.length
@@ -135,6 +159,16 @@ async function getOutputFromFile (path: string, startDate: Date, endDate: Date,
return { currentSize, output: output.reverse(), logTime }
}
+function lineHasTag (line: { tags?: string }, tagsOneOf: Set) {
+ if (!isArray(line.tags)) return false
+
+ for (const lineTag of line.tags) {
+ if (tagsOneOf.has(lineTag)) return true
+ }
+
+ return false
+}
+
function generateLogNameFilter (baseName: string) {
return new RegExp('^' + baseName.replace(/\.log$/, '') + '\\d*.log$')
}
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 @@
import express from 'express'
import { query } from 'express-validator'
+import { isStringArray } from '@server/helpers/custom-validators/search'
import { isValidLogLevel } from '../../helpers/custom-validators/logs'
-import { isDateValid } from '../../helpers/custom-validators/misc'
+import { isDateValid, toArray } from '../../helpers/custom-validators/misc'
import { logger } from '../../helpers/logger'
import { areValidationErrors } from './shared'
@@ -11,6 +12,10 @@ const getLogsValidator = [
query('level')
.optional()
.custom(isValidLogLevel).withMessage('Should have a valid level'),
+ query('tagsOneOf')
+ .optional()
+ .customSanitizer(toArray)
+ .custom(isStringArray).withMessage('Should have a valid one of tags array'),
query('endDate')
.optional()
.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 () {
expect(logsString.includes('video 5')).to.be.false
})
- it('Should get filter by level', async function () {
+ it('Should filter by level', async function () {
this.timeout(20000)
const now = new Date()
@@ -94,6 +94,27 @@ describe('Test logs', function () {
}
})
+ it('Should filter by tag', async function () {
+ const now = new Date()
+
+ const { uuid } = await server.videos.upload({ attributes: { name: 'video 6' } })
+ await waitJobs([ server ])
+
+ {
+ const body = await logsCommand.getLogs({ startDate: now, level: 'debug', tagsOneOf: [ 'toto' ] })
+ expect(body).to.have.lengthOf(0)
+ }
+
+ {
+ const body = await logsCommand.getLogs({ startDate: now, level: 'debug', tagsOneOf: [ uuid ] })
+ expect(body).to.not.have.lengthOf(0)
+
+ for (const line of body) {
+ expect(line.tags).to.contain(uuid)
+ }
+ }
+ })
+
it('Should log ping requests', async function () {
this.timeout(10000)
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 {
startDate: Date
endDate?: Date
level?: LogLevel
+ tagsOneOf?: string[]
}) {
- const { startDate, endDate, level } = options
+ const { startDate, endDate, tagsOneOf, level } = options
const path = '/api/v1/server/logs'
- return this.getRequestBody({
+ return this.getRequestBody({
...options,
path,
- query: { startDate, endDate, level },
+ query: { startDate, endDate, level, tagsOneOf },
implicitToken: true,
defaultExpectedStatus: HttpStatusCode.OK_200
})