</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>
@include peertube-select-container(150px);
}
- my-button,
- .peertube-select-container,
- ng-select {
+ > * {
@include margin-left(10px);
}
}
startDate: string
level: LogLevel
logType: 'audit' | 'standard'
+ tagsOneOf: string[] = []
constructor (
private logsService: LogsService,
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 () {
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'
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
[items]="availableItems"
[(ngModel)]="selectedItems"
(ngModelChange)="onModelChange()"
- i18n-placeholder placeholder="Enter a new tag"
+ [placeholder]="placeholder"
[maxSelectedItems]="5"
[clearable]="true"
[addTag]="true"
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'
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
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)
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 } = {
}
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
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'
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'),
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()
}
})
+ 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)
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
})