Verified Commit 64553e88 authored by Chocobozzz's avatar Chocobozzz
Browse files

Add ability to filter logs by tags

parent 12437298
......@@ -28,6 +28,8 @@
</ng-option>
</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>
</div>
......
......@@ -52,9 +52,7 @@
@include peertube-select-container(150px);
}
my-button,
.peertube-select-container,
ng-select {
> * {
@include margin-left(10px);
}
}
......
......@@ -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
setTimeout(() => {
this.logsElement.nativeElement.scrollIntoView({ block: 'end', inline: 'nearest' })
})
},
const tagsOneOf = this.tagsOneOf.length !== 0
? this.tagsOneOf
: undefined
this.logsService.getLogs({
isAuditLog: this.isAuditLog(),
level: this.level,
startDate: this.startDate,
tagsOneOf
}).subscribe({
next: logs => {
this.logs = logs
setTimeout(() => {
this.logsElement.nativeElement.scrollIntoView({ block: 'end', inline: 'nearest' })
})
},
error: err => this.notifier.error(err.message),
error: err => this.notifier.error(err.message),
complete: () => this.loading = false
})
complete: () => this.loading = false
})
}
isAuditLog () {
......
......@@ -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<any[]> {
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
......
......@@ -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"
......
......@@ -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 */ }
......
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<string>
}) {
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<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) {
return new RegExp('^' + baseName.replace(/\.log$/, '') + '\\d*.log$')
}
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'),
......
......@@ -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)
......
......@@ -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<any[]>({
...options,
path,
query: { startDate, endDate, level },
query: { startDate, endDate, level, tagsOneOf },
implicitToken: true,
defaultExpectedStatus: HttpStatusCode.OK_200
})
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment