]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/commitdiff
Add ability to filter logs by tags
authorChocobozzz <me@florianbigard.com>
Wed, 20 Oct 2021 12:23:32 +0000 (14:23 +0200)
committerChocobozzz <me@florianbigard.com>
Wed, 20 Oct 2021 12:33:38 +0000 (14:33 +0200)
client/src/app/+admin/system/logs/logs.component.html
client/src/app/+admin/system/logs/logs.component.scss
client/src/app/+admin/system/logs/logs.component.ts
client/src/app/+admin/system/logs/logs.service.ts
client/src/app/shared/shared-forms/select/select-tags.component.html
client/src/app/shared/shared-forms/select/select-tags.component.ts
server/controllers/api/server/logs.ts
server/middlewares/validators/logs.ts
server/tests/api/server/logs.ts
shared/extra-utils/logs/logs-command.ts

index b2c7f84bc76a89cdca86db565741f86601df86e9..18011b205705e6f5b94d9016d3d3ebfee60ac56d 100644 (file)
@@ -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>
 
index fefa7efc2b323c22b8cff2cee9ef4c938c2520de..be66d563ba5f9c206e4e165e14924258d5157ed3 100644 (file)
@@ -52,9 +52,7 @@
     @include peertube-select-container(150px);
   }
 
-  my-button,
-  .peertube-select-container,
-  ng-select {
+  > * {
     @include margin-left(10px);
   }
 }
index 865ab80a203b4a5842c07a1aa0b60c88847d21db..06237522a58b721ef7649d0d1696f7c11b216478 100644 (file)
@@ -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 () {
index 0c222cad2f12cc11d6191b23b1d681f2642049e5..ea7e08b9bf32deb5bd8c97de34f9786e40a64ee7 100644 (file)
@@ -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
index e1cd50882c02e65ee983671953cb8ffce9fd8aea..de6cee6db520e902e4b6b8c594e85704ca8a3d10 100644 (file)
@@ -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"
index 93d199037856bc1ffd04248ee6416c43c9fc4b78..bef04de8abee23562f917de3c4d1f5f62c42607f 100644 (file)
@@ -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 */ }
 
index dfd5491aa599470eb454ac54a4913a259298b254..8aa4b71907fa5e450a2cdef3f5e9619f4e64989d 100644 (file)
@@ -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$')
 }
index 03c1c4df15bb2ca255864f00cc241ed58f169d88..901d8ca64f684032b7be6ba3de65b12419d01c9d 100644 (file)
@@ -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'),
index bcd94dda30618e6310d31c774e372b16d51c4fd3..4fa13886e97bad549d6fb037e4ed40647986e1f2 100644 (file)
@@ -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)
 
index 5912e814f3fd4c91e683002b35352ac456b7b3a0..7b5c66c0ccb6fb989a1688a5900f769808d60ad8 100644 (file)
@@ -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
     })