Add ability to filter logs by tags
This commit is contained in:
parent
1243729899
commit
64553e8809
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,20 +52,28 @@ 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
|
||||||
next: logs => {
|
: undefined
|
||||||
this.logs = logs
|
|
||||||
|
|
||||||
setTimeout(() => {
|
this.logsService.getLogs({
|
||||||
this.logsElement.nativeElement.scrollIntoView({ block: 'end', inline: 'nearest' })
|
isAuditLog: this.isAuditLog(),
|
||||||
})
|
level: this.level,
|
||||||
},
|
startDate: this.startDate,
|
||||||
|
tagsOneOf
|
||||||
|
}).subscribe({
|
||||||
|
next: logs => {
|
||||||
|
this.logs = logs
|
||||||
|
|
||||||
error: err => this.notifier.error(err.message),
|
setTimeout(() => {
|
||||||
|
this.logsElement.nativeElement.scrollIntoView({ block: 'end', inline: 'nearest' })
|
||||||
complete: () => this.loading = false
|
|
||||||
})
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
error: err => this.notifier.error(err.message),
|
||||||
|
|
||||||
|
complete: () => this.loading = false
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
isAuditLog () {
|
isAuditLog () {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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 */ }
|
||||||
|
|
||||||
|
|
|
@ -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$')
|
||||||
}
|
}
|
||||||
|
|
|
@ -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'),
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
})
|
})
|
||||||
|
|
Loading…
Reference in New Issue