Add ability to filter logs by tags

This commit is contained in:
Chocobozzz 2021-10-20 14:23:32 +02:00
parent 1243729899
commit 64553e8809
No known key found for this signature in database
GPG Key ID: 583A612D890159BE
10 changed files with 101 additions and 27 deletions

View File

@ -28,6 +28,8 @@
</ng-option> </ng-option>
</ng-select> </ng-select>
<my-select-tags i18n-placeholder placeholder="Filter logs by tags" [(ngModel)]="tagsOneOf" (ngModelChange)="refresh()"></my-select-tags>
<my-button i18n-label label="Refresh" icon="refresh" (click)="refresh()"></my-button> <my-button i18n-label label="Refresh" icon="refresh" (click)="refresh()"></my-button>
</div> </div>

View File

@ -52,9 +52,7 @@
@include peertube-select-container(150px); @include peertube-select-container(150px);
} }
my-button, > * {
.peertube-select-container,
ng-select {
@include margin-left(10px); @include margin-left(10px);
} }
} }

View File

@ -23,6 +23,7 @@ export class LogsComponent implements OnInit {
startDate: string startDate: string
level: LogLevel level: LogLevel
logType: 'audit' | 'standard' logType: 'audit' | 'standard'
tagsOneOf: string[] = []
constructor ( constructor (
private logsService: LogsService, private logsService: LogsService,
@ -51,8 +52,16 @@ export class LogsComponent implements OnInit {
load () { load () {
this.loading = true this.loading = true
this.logsService.getLogs({ isAuditLog: this.isAuditLog(), level: this.level, startDate: this.startDate }) const tagsOneOf = this.tagsOneOf.length !== 0
.subscribe({ ? this.tagsOneOf
: undefined
this.logsService.getLogs({
isAuditLog: this.isAuditLog(),
level: this.level,
startDate: this.startDate,
tagsOneOf
}).subscribe({
next: logs => { next: logs => {
this.logs = logs this.logs = logs

View File

@ -2,7 +2,7 @@ import { Observable } from 'rxjs'
import { catchError, map } from 'rxjs/operators' import { catchError, map } from 'rxjs/operators'
import { HttpClient, HttpParams } from '@angular/common/http' import { HttpClient, HttpParams } from '@angular/common/http'
import { Injectable } from '@angular/core' import { Injectable } from '@angular/core'
import { RestExtractor } from '@app/core' import { RestExtractor, RestService } from '@app/core'
import { LogLevel } from '@shared/models' import { LogLevel } from '@shared/models'
import { environment } from '../../../../environments/environment' import { environment } from '../../../../environments/environment'
import { LogRow } from './log-row.model' import { LogRow } from './log-row.model'
@ -14,22 +14,25 @@ export class LogsService {
constructor ( constructor (
private authHttp: HttpClient, private authHttp: HttpClient,
private restService: RestService,
private restExtractor: RestExtractor private restExtractor: RestExtractor
) {} ) {}
getLogs (options: { getLogs (options: {
isAuditLog: boolean isAuditLog: boolean
startDate: string startDate: string
tagsOneOf?: string[]
level?: LogLevel level?: LogLevel
endDate?: string endDate?: string
}): Observable<any[]> { }): Observable<any[]> {
const { isAuditLog, startDate } = options const { isAuditLog, startDate, endDate, tagsOneOf } = options
let params = new HttpParams() let params = new HttpParams()
params = params.append('startDate', startDate) params = params.append('startDate', startDate)
if (!isAuditLog) params = params.append('level', options.level) 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 const path = isAuditLog
? LogsService.BASE_AUDIT_LOG_URL ? LogsService.BASE_AUDIT_LOG_URL

View File

@ -2,7 +2,7 @@
[items]="availableItems" [items]="availableItems"
[(ngModel)]="selectedItems" [(ngModel)]="selectedItems"
(ngModelChange)="onModelChange()" (ngModelChange)="onModelChange()"
i18n-placeholder placeholder="Enter a new tag" [placeholder]="placeholder"
[maxSelectedItems]="5" [maxSelectedItems]="5"
[clearable]="true" [clearable]="true"
[addTag]="true" [addTag]="true"

View File

@ -16,6 +16,7 @@ import { NG_VALUE_ACCESSOR, ControlValueAccessor } from '@angular/forms'
export class SelectTagsComponent implements ControlValueAccessor { export class SelectTagsComponent implements ControlValueAccessor {
@Input() availableItems: string[] = [] @Input() availableItems: string[] = []
@Input() selectedItems: string[] = [] @Input() selectedItems: string[] = []
@Input() placeholder = $localize`Enter a new tag`
propagateChange = (_: any) => { /* empty */ } propagateChange = (_: any) => { /* empty */ }

View File

@ -1,6 +1,7 @@
import express from 'express' import express from 'express'
import { readdir, readFile } from 'fs-extra' import { readdir, readFile } from 'fs-extra'
import { join } from 'path' import { join } from 'path'
import { isArray } from '@server/helpers/custom-validators/misc'
import { logger, mtimeSortFilesDesc } from '@server/helpers/logger' import { logger, mtimeSortFilesDesc } from '@server/helpers/logger'
import { LogLevel } from '../../../../shared/models/server/log-level.type' import { LogLevel } from '../../../../shared/models/server/log-level.type'
import { UserRight } from '../../../../shared/models/users' import { UserRight } from '../../../../shared/models/users'
@ -51,20 +52,27 @@ async function getLogs (req: express.Request, res: express.Response) {
startDateQuery: req.query.startDate, startDateQuery: req.query.startDate,
endDateQuery: req.query.endDate, endDateQuery: req.query.endDate,
level: req.query.level || 'info', level: req.query.level || 'info',
tagsOneOf: req.query.tagsOneOf,
nameFilter: logNameFilter nameFilter: logNameFilter
}) })
return res.json(output).end() return res.json(output)
} }
async function generateOutput (options: { async function generateOutput (options: {
startDateQuery: string startDateQuery: string
endDateQuery?: string endDateQuery?: string
level: LogLevel level: LogLevel
nameFilter: RegExp nameFilter: RegExp
tagsOneOf?: string[]
}) { }) {
const { startDateQuery, level, nameFilter } = options 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 logFiles = await readdir(CONFIG.STORAGE.LOG_DIR)
const sortedLogFiles = await mtimeSortFilesDesc(logFiles, CONFIG.STORAGE.LOG_DIR) const sortedLogFiles = await mtimeSortFilesDesc(logFiles, CONFIG.STORAGE.LOG_DIR)
let currentSize = 0 let currentSize = 0
@ -80,7 +88,7 @@ async function generateOutput (options: {
const path = join(CONFIG.STORAGE.LOG_DIR, meta.file) const path = join(CONFIG.STORAGE.LOG_DIR, meta.file)
logger.debug('Opening %s to fetch logs.', path) 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 if (!result.output) break
output = result.output.concat(output) output = result.output.concat(output)
@ -92,9 +100,20 @@ async function generateOutput (options: {
return output 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<string>
}) {
const { path, startDate, endDate, level, tagsOneOf } = options
const startTime = startDate.getTime() const startTime = startDate.getTime()
const endTime = endDate.getTime() const endTime = endDate.getTime()
let currentSize = options.currentSize
let logTime: number let logTime: number
const logsLevel: { [ id in LogLevel ]: 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() 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) output.push(log)
currentSize += line.length currentSize += line.length
@ -135,6 +159,16 @@ async function getOutputFromFile (path: string, startDate: Date, endDate: Date,
return { currentSize, output: output.reverse(), logTime } return { currentSize, output: output.reverse(), logTime }
} }
function lineHasTag (line: { tags?: string }, tagsOneOf: Set<string>) {
if (!isArray(line.tags)) return false
for (const lineTag of line.tags) {
if (tagsOneOf.has(lineTag)) return true
}
return false
}
function generateLogNameFilter (baseName: string) { function generateLogNameFilter (baseName: string) {
return new RegExp('^' + baseName.replace(/\.log$/, '') + '\\d*.log$') return new RegExp('^' + baseName.replace(/\.log$/, '') + '\\d*.log$')
} }

View File

@ -1,7 +1,8 @@
import express from 'express' import express from 'express'
import { query } from 'express-validator' import { query } from 'express-validator'
import { isStringArray } from '@server/helpers/custom-validators/search'
import { isValidLogLevel } from '../../helpers/custom-validators/logs' 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 { logger } from '../../helpers/logger'
import { areValidationErrors } from './shared' import { areValidationErrors } from './shared'
@ -11,6 +12,10 @@ const getLogsValidator = [
query('level') query('level')
.optional() .optional()
.custom(isValidLogLevel).withMessage('Should have a valid level'), .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') query('endDate')
.optional() .optional()
.custom(isDateValid).withMessage('Should have an end date that conforms to ISO 8601'), .custom(isDateValid).withMessage('Should have an end date that conforms to ISO 8601'),

View File

@ -71,7 +71,7 @@ describe('Test logs', function () {
expect(logsString.includes('video 5')).to.be.false 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) this.timeout(20000)
const now = new Date() 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 () { it('Should log ping requests', async function () {
this.timeout(10000) this.timeout(10000)

View File

@ -8,15 +8,16 @@ export class LogsCommand extends AbstractCommand {
startDate: Date startDate: Date
endDate?: Date endDate?: Date
level?: LogLevel level?: LogLevel
tagsOneOf?: string[]
}) { }) {
const { startDate, endDate, level } = options const { startDate, endDate, tagsOneOf, level } = options
const path = '/api/v1/server/logs' const path = '/api/v1/server/logs'
return this.getRequestBody({ return this.getRequestBody<any[]>({
...options, ...options,
path, path,
query: { startDate, endDate, level }, query: { startDate, endDate, level, tagsOneOf },
implicitToken: true, implicitToken: true,
defaultExpectedStatus: HttpStatusCode.OK_200 defaultExpectedStatus: HttpStatusCode.OK_200
}) })