mirror of https://github.com/Chocobozzz/PeerTube
Add ability to filter logs by tags
parent
1243729899
commit
64553e8809
|
@ -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
|
||||
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 () {
|
||||
|
|
|
@ -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 */ }
|
||||
|
||||
|
|
|
@ -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<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$')
|
||||
}
|
||||
|
|
|
@ -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'),
|
||||
|
|
|
@ -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
|
||||
})
|
||||
|
|
Loading…
Reference in New Issue