Serve audit logs to client
This commit is contained in:
parent
92e0f42e8c
commit
566c125d6e
|
@ -15,7 +15,7 @@
|
||||||
|
|
||||||
<ng-template pTemplate="body" let-expanded="expanded" let-videoAbuse>
|
<ng-template pTemplate="body" let-expanded="expanded" let-videoAbuse>
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td class="expand-cell">
|
||||||
<span class="expander" [pRowToggler]="videoAbuse">
|
<span class="expander" [pRowToggler]="videoAbuse">
|
||||||
<i [ngClass]="expanded ? 'glyphicon glyphicon-menu-down' : 'glyphicon glyphicon-menu-right'"></i>
|
<i [ngClass]="expanded ? 'glyphicon glyphicon-menu-down' : 'glyphicon glyphicon-menu-right'"></i>
|
||||||
</span>
|
</span>
|
||||||
|
|
|
@ -15,7 +15,7 @@
|
||||||
|
|
||||||
<ng-template pTemplate="body" let-videoBlacklist let-expanded="expanded">
|
<ng-template pTemplate="body" let-videoBlacklist let-expanded="expanded">
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td class="expand-cell">
|
||||||
<span *ngIf="videoBlacklist.reason" class="expander" [pRowToggler]="videoBlacklist">
|
<span *ngIf="videoBlacklist.reason" class="expander" [pRowToggler]="videoBlacklist">
|
||||||
<i [ngClass]="expanded ? 'glyphicon glyphicon-menu-down' : 'glyphicon glyphicon-menu-right'"></i>
|
<i [ngClass]="expanded ? 'glyphicon glyphicon-menu-down' : 'glyphicon glyphicon-menu-right'"></i>
|
||||||
</span>
|
</span>
|
||||||
|
|
|
@ -38,7 +38,7 @@
|
||||||
|
|
||||||
<ng-template pTemplate="body" let-expanded="expanded" let-job>
|
<ng-template pTemplate="body" let-expanded="expanded" let-job>
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td class="expand-cell">
|
||||||
<span class="expander" [pRowToggler]="job">
|
<span class="expander" [pRowToggler]="job">
|
||||||
<i [ngClass]="expanded ? 'glyphicon glyphicon-menu-down' : 'glyphicon glyphicon-menu-right'"></i>
|
<i [ngClass]="expanded ? 'glyphicon glyphicon-menu-down' : 'glyphicon glyphicon-menu-right'"></i>
|
||||||
</span>
|
</span>
|
||||||
|
|
|
@ -8,6 +8,10 @@ export class LogRow {
|
||||||
message: string
|
message: string
|
||||||
meta: string
|
meta: string
|
||||||
|
|
||||||
|
by: string
|
||||||
|
domain: string
|
||||||
|
action: string
|
||||||
|
|
||||||
constructor (row: any) {
|
constructor (row: any) {
|
||||||
this.date = new Date(row.timestamp)
|
this.date = new Date(row.timestamp)
|
||||||
this.localeDate = this.date.toLocaleString()
|
this.localeDate = this.date.toLocaleString()
|
||||||
|
@ -17,5 +21,20 @@ export class LogRow {
|
||||||
const metaObj = omit(row, 'timestamp', 'level', 'message', 'label')
|
const metaObj = omit(row, 'timestamp', 'level', 'message', 'label')
|
||||||
|
|
||||||
if (Object.keys(metaObj).length !== 0) this.meta = JSON.stringify(metaObj, undefined, 2)
|
if (Object.keys(metaObj).length !== 0) this.meta = JSON.stringify(metaObj, undefined, 2)
|
||||||
|
|
||||||
|
if (row.level === 'audit') {
|
||||||
|
try {
|
||||||
|
const message = JSON.parse(row.message)
|
||||||
|
|
||||||
|
this.by = message.user
|
||||||
|
this.domain = message.domain
|
||||||
|
this.action = message.action
|
||||||
|
|
||||||
|
this.meta = JSON.stringify(message, null, 2)
|
||||||
|
this.message = ''
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Cannot parse audit message.', err)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,17 @@
|
||||||
<div class="header">
|
<div class="header">
|
||||||
|
<div class="peertube-select-container">
|
||||||
|
<select [(ngModel)]="logType" (ngModelChange)="refresh()">
|
||||||
|
<option *ngFor="let logTypeChoice of logTypeChoices" [value]="logTypeChoice.id">{{ logTypeChoice.label }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="peertube-select-container">
|
<div class="peertube-select-container">
|
||||||
<select [(ngModel)]="startDate" (ngModelChange)="refresh()">
|
<select [(ngModel)]="startDate" (ngModelChange)="refresh()">
|
||||||
<option *ngFor="let timeChoice of timeChoices" [value]="timeChoice.id">{{ timeChoice.label }}</option>
|
<option *ngFor="let timeChoice of timeChoices" [value]="timeChoice.id">{{ timeChoice.label }}</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="peertube-select-container">
|
<div class="peertube-select-container" *ngIf="!isAuditLog()">
|
||||||
<select [(ngModel)]="level" (ngModelChange)="refresh()">
|
<select [(ngModel)]="level" (ngModelChange)="refresh()">
|
||||||
<option *ngFor="let levelChoice of levelChoices" [value]="levelChoice.id">{{ levelChoice.label }}</option>
|
<option *ngFor="let levelChoice of levelChoices" [value]="levelChoice.id">{{ levelChoice.label }}</option>
|
||||||
</select>
|
</select>
|
||||||
|
@ -23,9 +29,12 @@
|
||||||
|
|
||||||
<span class="log-date">[{{ log.localeDate }}]</span>
|
<span class="log-date">[{{ log.localeDate }}]</span>
|
||||||
|
|
||||||
|
<strong class="log-by" *ngIf="log.by" i18n>By {{ log.by }} -></strong>
|
||||||
|
<strong class="log-domain-action" *ngIf="log.domain">{{ log.domain }} -> {{ log.action }}</strong>
|
||||||
|
|
||||||
{{ log.message }}
|
{{ log.message }}
|
||||||
|
|
||||||
{{ log.meta }}
|
<pre>{{ log.meta }}</pre>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -23,6 +23,10 @@
|
||||||
margin-right: 5px;
|
margin-right: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.log-by {
|
||||||
|
margin: 0 5px;
|
||||||
|
}
|
||||||
|
|
||||||
.warn {
|
.warn {
|
||||||
color: $orange-color;
|
color: $orange-color;
|
||||||
}
|
}
|
||||||
|
@ -30,6 +34,12 @@
|
||||||
.error {
|
.error {
|
||||||
color: $red;
|
color: $red;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
margin-bottom: 5px;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-wrap: break-word;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
|
|
|
@ -17,9 +17,11 @@ export class LogsComponent implements OnInit {
|
||||||
logs: LogRow[] = []
|
logs: LogRow[] = []
|
||||||
timeChoices: { id: string, label: string }[] = []
|
timeChoices: { id: string, label: string }[] = []
|
||||||
levelChoices: { id: LogLevel, label: string }[] = []
|
levelChoices: { id: LogLevel, label: string }[] = []
|
||||||
|
logTypeChoices: { id: 'audit' | 'standard', label: string }[] = []
|
||||||
|
|
||||||
startDate: string
|
startDate: string
|
||||||
level: LogLevel
|
level: LogLevel
|
||||||
|
logType: 'audit' | 'standard'
|
||||||
|
|
||||||
constructor (
|
constructor (
|
||||||
private logsService: LogsService,
|
private logsService: LogsService,
|
||||||
|
@ -30,6 +32,7 @@ export class LogsComponent implements OnInit {
|
||||||
ngOnInit (): void {
|
ngOnInit (): void {
|
||||||
this.buildTimeChoices()
|
this.buildTimeChoices()
|
||||||
this.buildLevelChoices()
|
this.buildLevelChoices()
|
||||||
|
this.buildLogTypeChoices()
|
||||||
|
|
||||||
this.load()
|
this.load()
|
||||||
}
|
}
|
||||||
|
@ -42,7 +45,7 @@ export class LogsComponent implements OnInit {
|
||||||
load () {
|
load () {
|
||||||
this.loading = true
|
this.loading = true
|
||||||
|
|
||||||
this.logsService.getLogs(this.level, this.startDate)
|
this.logsService.getLogs({ isAuditLog: this.isAuditLog(), level: this.level, startDate: this.startDate })
|
||||||
.subscribe(
|
.subscribe(
|
||||||
logs => {
|
logs => {
|
||||||
this.logs = logs
|
this.logs = logs
|
||||||
|
@ -58,6 +61,10 @@ export class LogsComponent implements OnInit {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isAuditLog () {
|
||||||
|
return this.logType === 'audit'
|
||||||
|
}
|
||||||
|
|
||||||
buildTimeChoices () {
|
buildTimeChoices () {
|
||||||
const lastHour = new Date()
|
const lastHour = new Date()
|
||||||
lastHour.setHours(lastHour.getHours() - 1)
|
lastHour.setHours(lastHour.getHours() - 1)
|
||||||
|
@ -108,4 +115,19 @@ export class LogsComponent implements OnInit {
|
||||||
|
|
||||||
this.level = 'warn'
|
this.level = 'warn'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
buildLogTypeChoices () {
|
||||||
|
this.logTypeChoices = [
|
||||||
|
{
|
||||||
|
id: 'standard',
|
||||||
|
label: this.i18n('Standard logs')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'audit',
|
||||||
|
label: this.i18n('Audit logs')
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
this.logType = 'audit'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,6 +10,7 @@ import { LogLevel } from '@shared/models/server/log-level.type'
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class LogsService {
|
export class LogsService {
|
||||||
private static BASE_LOG_URL = environment.apiUrl + '/api/v1/server/logs'
|
private static BASE_LOG_URL = environment.apiUrl + '/api/v1/server/logs'
|
||||||
|
private static BASE_AUDIT_LOG_URL = environment.apiUrl + '/api/v1/server/audit-logs'
|
||||||
|
|
||||||
constructor (
|
constructor (
|
||||||
private authHttp: HttpClient,
|
private authHttp: HttpClient,
|
||||||
|
@ -17,14 +18,25 @@ export class LogsService {
|
||||||
private restExtractor: RestExtractor
|
private restExtractor: RestExtractor
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
getLogs (level: LogLevel, startDate: string, endDate?: string): Observable<any[]> {
|
getLogs (options: {
|
||||||
|
isAuditLog: boolean,
|
||||||
|
startDate: string,
|
||||||
|
level?: LogLevel,
|
||||||
|
endDate?: string
|
||||||
|
}): Observable<any[]> {
|
||||||
|
const { isAuditLog, startDate } = options
|
||||||
|
|
||||||
let params = new HttpParams()
|
let params = new HttpParams()
|
||||||
params = params.append('startDate', startDate)
|
params = params.append('startDate', startDate)
|
||||||
params = params.append('level', level)
|
|
||||||
|
|
||||||
if (endDate) params.append('endDate', endDate)
|
if (!isAuditLog) params = params.append('level', options.level)
|
||||||
|
if (options.endDate) params.append('endDate', options.endDate)
|
||||||
|
|
||||||
return this.authHttp.get<any[]>(LogsService.BASE_LOG_URL, { params })
|
const path = isAuditLog
|
||||||
|
? LogsService.BASE_AUDIT_LOG_URL
|
||||||
|
: LogsService.BASE_LOG_URL
|
||||||
|
|
||||||
|
return this.authHttp.get<any[]>(path, { params })
|
||||||
.pipe(
|
.pipe(
|
||||||
map(rows => rows.map(r => new LogRow(r))),
|
map(rows => rows.map(r => new LogRow(r))),
|
||||||
catchError(err => this.restExtractor.handleError(err))
|
catchError(err => this.restExtractor.handleError(err))
|
||||||
|
|
|
@ -49,7 +49,7 @@
|
||||||
<ng-template pTemplate="body" let-expanded="expanded" let-user>
|
<ng-template pTemplate="body" let-expanded="expanded" let-user>
|
||||||
|
|
||||||
<tr [pSelectableRow]="user" [ngClass]="{ banned: user.blocked }">
|
<tr [pSelectableRow]="user" [ngClass]="{ banned: user.blocked }">
|
||||||
<td>
|
<td class="expand-cell">
|
||||||
<p-tableCheckbox [value]="user"></p-tableCheckbox>
|
<p-tableCheckbox [value]="user"></p-tableCheckbox>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
|
|
|
@ -37,7 +37,7 @@ p-table {
|
||||||
td {
|
td {
|
||||||
padding-left: 15px !important;
|
padding-left: 15px !important;
|
||||||
|
|
||||||
&:not(.action-cell) {
|
&:not(.action-cell):not(.expand-cell) {
|
||||||
overflow: hidden !important;
|
overflow: hidden !important;
|
||||||
text-overflow: ellipsis !important;
|
text-overflow: ellipsis !important;
|
||||||
white-space: nowrap !important;
|
white-space: nowrap !important;
|
||||||
|
|
|
@ -3,11 +3,12 @@ import { UserRight } from '../../../../shared/models/users'
|
||||||
import { asyncMiddleware, authenticate, ensureUserHasRight } from '../../../middlewares'
|
import { asyncMiddleware, authenticate, ensureUserHasRight } from '../../../middlewares'
|
||||||
import { mtimeSortFilesDesc } from '../../../../shared/core-utils/logs/logs'
|
import { mtimeSortFilesDesc } from '../../../../shared/core-utils/logs/logs'
|
||||||
import { readdir, readFile } from 'fs-extra'
|
import { readdir, readFile } from 'fs-extra'
|
||||||
import { MAX_LOGS_OUTPUT_CHARACTERS } from '../../../initializers/constants'
|
import { AUDIT_LOG_FILENAME, MAX_LOGS_OUTPUT_CHARACTERS, LOG_FILENAME } from '../../../initializers/constants'
|
||||||
import { join } from 'path'
|
import { join } from 'path'
|
||||||
import { getLogsValidator } from '../../../middlewares/validators/logs'
|
import { getAuditLogsValidator, getLogsValidator } from '../../../middlewares/validators/logs'
|
||||||
import { LogLevel } from '../../../../shared/models/server/log-level.type'
|
import { LogLevel } from '../../../../shared/models/server/log-level.type'
|
||||||
import { CONFIG } from '../../../initializers/config'
|
import { CONFIG } from '../../../initializers/config'
|
||||||
|
import { logger } from '@server/helpers/logger'
|
||||||
|
|
||||||
const logsRouter = express.Router()
|
const logsRouter = express.Router()
|
||||||
|
|
||||||
|
@ -18,6 +19,13 @@ logsRouter.get('/logs',
|
||||||
asyncMiddleware(getLogs)
|
asyncMiddleware(getLogs)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
logsRouter.get('/audit-logs',
|
||||||
|
authenticate,
|
||||||
|
ensureUserHasRight(UserRight.MANAGE_LOGS),
|
||||||
|
getAuditLogsValidator,
|
||||||
|
asyncMiddleware(getAuditLogs)
|
||||||
|
)
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
@ -26,18 +34,50 @@ export {
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
const auditLogNameFilter = generateLogNameFilter(AUDIT_LOG_FILENAME)
|
||||||
|
async function getAuditLogs (req: express.Request, res: express.Response) {
|
||||||
|
const output = await generateOutput({
|
||||||
|
startDateQuery: req.query.startDate,
|
||||||
|
endDateQuery: req.query.endDate,
|
||||||
|
level: 'audit',
|
||||||
|
nameFilter: auditLogNameFilter
|
||||||
|
})
|
||||||
|
|
||||||
|
return res.json(output).end()
|
||||||
|
}
|
||||||
|
|
||||||
|
const logNameFilter = generateLogNameFilter(LOG_FILENAME)
|
||||||
async function getLogs (req: express.Request, res: express.Response) {
|
async function getLogs (req: express.Request, res: express.Response) {
|
||||||
|
const output = await generateOutput({
|
||||||
|
startDateQuery: req.query.startDate,
|
||||||
|
endDateQuery: req.query.endDate,
|
||||||
|
level: req.query.level || 'info',
|
||||||
|
nameFilter: logNameFilter
|
||||||
|
})
|
||||||
|
|
||||||
|
return res.json(output).end()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function generateOutput (options: {
|
||||||
|
startDateQuery: string,
|
||||||
|
endDateQuery?: string,
|
||||||
|
level: LogLevel,
|
||||||
|
nameFilter: RegExp
|
||||||
|
}) {
|
||||||
|
const { startDateQuery, level, nameFilter } = options
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
const startDate = new Date(req.query.startDate)
|
const startDate = new Date(startDateQuery)
|
||||||
const endDate = req.query.endDate ? new Date(req.query.endDate) : new Date()
|
const endDate = options.endDateQuery ? new Date(options.endDateQuery) : new Date()
|
||||||
const level: LogLevel = req.query.level || 'info'
|
|
||||||
|
|
||||||
let output: string[] = []
|
let output: string[] = []
|
||||||
|
|
||||||
for (const meta of sortedLogFiles) {
|
for (const meta of sortedLogFiles) {
|
||||||
|
if (nameFilter.exec(meta.file) === null) continue
|
||||||
|
|
||||||
const path = join(CONFIG.STORAGE.LOG_DIR, meta.file)
|
const path = join(CONFIG.STORAGE.LOG_DIR, meta.file)
|
||||||
|
|
||||||
const result = await getOutputFromFile(path, startDate, endDate, level, currentSize)
|
const result = await getOutputFromFile(path, startDate, endDate, level, currentSize)
|
||||||
|
@ -49,7 +89,7 @@ async function getLogs (req: express.Request, res: express.Response) {
|
||||||
if (currentSize > MAX_LOGS_OUTPUT_CHARACTERS || (result.logTime && result.logTime < startDate.getTime())) break
|
if (currentSize > MAX_LOGS_OUTPUT_CHARACTERS || (result.logTime && result.logTime < startDate.getTime())) break
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.json(output).end()
|
return output
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getOutputFromFile (path: string, startDate: Date, endDate: Date, level: LogLevel, currentSize: number) {
|
async function getOutputFromFile (path: string, startDate: Date, endDate: Date, level: LogLevel, currentSize: number) {
|
||||||
|
@ -58,6 +98,7 @@ async function getOutputFromFile (path: string, startDate: Date, endDate: Date,
|
||||||
let logTime: number
|
let logTime: number
|
||||||
|
|
||||||
const logsLevel: { [ id in LogLevel ]: number } = {
|
const logsLevel: { [ id in LogLevel ]: number } = {
|
||||||
|
audit: -1,
|
||||||
debug: 0,
|
debug: 0,
|
||||||
info: 1,
|
info: 1,
|
||||||
warn: 2,
|
warn: 2,
|
||||||
|
@ -93,3 +134,7 @@ async function getOutputFromFile (path: string, startDate: Date, endDate: Date,
|
||||||
|
|
||||||
return { currentSize, output: output.reverse(), logTime }
|
return { currentSize, output: output.reverse(), logTime }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function generateLogNameFilter (baseName: string) {
|
||||||
|
return new RegExp('^' + baseName.replace(/\.log$/, '') + '\d*.log$')
|
||||||
|
}
|
||||||
|
|
|
@ -9,6 +9,7 @@ import { User, VideoAbuse, VideoChannel, VideoDetails, VideoImport } from '../..
|
||||||
import { VideoComment } from '../../shared/models/videos/video-comment.model'
|
import { VideoComment } from '../../shared/models/videos/video-comment.model'
|
||||||
import { CustomConfig } from '../../shared/models/server/custom-config.model'
|
import { CustomConfig } from '../../shared/models/server/custom-config.model'
|
||||||
import { CONFIG } from '../initializers/config'
|
import { CONFIG } from '../initializers/config'
|
||||||
|
import { AUDIT_LOG_FILENAME } from '@server/initializers/constants'
|
||||||
|
|
||||||
function getAuditIdFromRes (res: express.Response) {
|
function getAuditIdFromRes (res: express.Response) {
|
||||||
return res.locals.oauth.token.User.username
|
return res.locals.oauth.token.User.username
|
||||||
|
@ -29,7 +30,7 @@ const auditLogger = winston.createLogger({
|
||||||
levels: { audit: 0 },
|
levels: { audit: 0 },
|
||||||
transports: [
|
transports: [
|
||||||
new winston.transports.File({
|
new winston.transports.File({
|
||||||
filename: path.join(CONFIG.STORAGE.LOG_DIR, 'peertube-audit.log'),
|
filename: path.join(CONFIG.STORAGE.LOG_DIR, AUDIT_LOG_FILENAME),
|
||||||
level: 'audit',
|
level: 'audit',
|
||||||
maxsize: 5242880,
|
maxsize: 5242880,
|
||||||
maxFiles: 5,
|
maxFiles: 5,
|
||||||
|
|
|
@ -5,6 +5,7 @@ import * as winston from 'winston'
|
||||||
import { FileTransportOptions } from 'winston/lib/winston/transports'
|
import { FileTransportOptions } from 'winston/lib/winston/transports'
|
||||||
import { CONFIG } from '../initializers/config'
|
import { CONFIG } from '../initializers/config'
|
||||||
import { omit } from 'lodash'
|
import { omit } from 'lodash'
|
||||||
|
import { LOG_FILENAME } from '@server/initializers/constants'
|
||||||
|
|
||||||
const label = CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT
|
const label = CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT
|
||||||
|
|
||||||
|
@ -58,7 +59,7 @@ const labelFormatter = winston.format.label({
|
||||||
})
|
})
|
||||||
|
|
||||||
const fileLoggerOptions: FileTransportOptions = {
|
const fileLoggerOptions: FileTransportOptions = {
|
||||||
filename: path.join(CONFIG.STORAGE.LOG_DIR, 'peertube.log'),
|
filename: path.join(CONFIG.STORAGE.LOG_DIR, LOG_FILENAME),
|
||||||
handleExceptions: true,
|
handleExceptions: true,
|
||||||
format: winston.format.combine(
|
format: winston.format.combine(
|
||||||
winston.format.timestamp(),
|
winston.format.timestamp(),
|
||||||
|
|
|
@ -603,6 +603,8 @@ const FEEDS = {
|
||||||
}
|
}
|
||||||
|
|
||||||
const MAX_LOGS_OUTPUT_CHARACTERS = 10 * 1000 * 1000
|
const MAX_LOGS_OUTPUT_CHARACTERS = 10 * 1000 * 1000
|
||||||
|
const LOG_FILENAME = 'peertube.log'
|
||||||
|
const AUDIT_LOG_FILENAME = 'peertube-audit.log'
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@ -684,6 +686,7 @@ export {
|
||||||
BCRYPT_SALT_SIZE,
|
BCRYPT_SALT_SIZE,
|
||||||
TRACKER_RATE_LIMITS,
|
TRACKER_RATE_LIMITS,
|
||||||
FILES_CACHE,
|
FILES_CACHE,
|
||||||
|
LOG_FILENAME,
|
||||||
CONSTRAINTS_FIELDS,
|
CONSTRAINTS_FIELDS,
|
||||||
EMBED_SIZE,
|
EMBED_SIZE,
|
||||||
REDUNDANCY,
|
REDUNDANCY,
|
||||||
|
@ -693,6 +696,7 @@ export {
|
||||||
OAUTH_LIFETIME,
|
OAUTH_LIFETIME,
|
||||||
CUSTOM_HTML_TAG_COMMENTS,
|
CUSTOM_HTML_TAG_COMMENTS,
|
||||||
BROADCAST_CONCURRENCY,
|
BROADCAST_CONCURRENCY,
|
||||||
|
AUDIT_LOG_FILENAME,
|
||||||
PAGINATION,
|
PAGINATION,
|
||||||
ACTOR_FOLLOW_SCORE,
|
ACTOR_FOLLOW_SCORE,
|
||||||
PREVIEWS_SIZE,
|
PREVIEWS_SIZE,
|
||||||
|
|
|
@ -24,8 +24,25 @@ const getLogsValidator = [
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
const getAuditLogsValidator = [
|
||||||
|
query('startDate')
|
||||||
|
.custom(isDateValid).withMessage('Should have a valid start date'),
|
||||||
|
query('endDate')
|
||||||
|
.optional()
|
||||||
|
.custom(isDateValid).withMessage('Should have a valid end date'),
|
||||||
|
|
||||||
|
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||||
|
logger.debug('Checking getAuditLogsValidator parameters.', { parameters: req.query })
|
||||||
|
|
||||||
|
if (areValidationErrors(req, res)) return
|
||||||
|
|
||||||
|
return next()
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
export {
|
export {
|
||||||
getLogsValidator
|
getLogsValidator,
|
||||||
|
getAuditLogsValidator
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,17 +2,10 @@
|
||||||
|
|
||||||
import * as chai from 'chai'
|
import * as chai from 'chai'
|
||||||
import 'mocha'
|
import 'mocha'
|
||||||
import {
|
import { cleanupTests, flushAndRunServer, ServerInfo, setAccessTokensToServers } from '../../../../shared/extra-utils/index'
|
||||||
flushTests,
|
|
||||||
killallServers,
|
|
||||||
flushAndRunServer,
|
|
||||||
ServerInfo,
|
|
||||||
setAccessTokensToServers,
|
|
||||||
cleanupTests
|
|
||||||
} from '../../../../shared/extra-utils/index'
|
|
||||||
import { waitJobs } from '../../../../shared/extra-utils/server/jobs'
|
import { waitJobs } from '../../../../shared/extra-utils/server/jobs'
|
||||||
import { uploadVideo } from '../../../../shared/extra-utils/videos/videos'
|
import { uploadVideo } from '../../../../shared/extra-utils/videos/videos'
|
||||||
import { getLogs } from '../../../../shared/extra-utils/logs/logs'
|
import { getAuditLogs, getLogs } from '../../../../shared/extra-utils/logs/logs'
|
||||||
|
|
||||||
const expect = chai.expect
|
const expect = chai.expect
|
||||||
|
|
||||||
|
@ -26,69 +19,123 @@ describe('Test logs', function () {
|
||||||
await setAccessTokensToServers([ server ])
|
await setAccessTokensToServers([ server ])
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should get logs with a start date', async function () {
|
describe('With the standard log file', function () {
|
||||||
this.timeout(10000)
|
it('Should get logs with a start date', async function () {
|
||||||
|
this.timeout(10000)
|
||||||
|
|
||||||
await uploadVideo(server.url, server.accessToken, { name: 'video 1' })
|
await uploadVideo(server.url, server.accessToken, { name: 'video 1' })
|
||||||
await waitJobs([ server ])
|
await waitJobs([ server ])
|
||||||
|
|
||||||
const now = new Date()
|
const now = new Date()
|
||||||
|
|
||||||
await uploadVideo(server.url, server.accessToken, { name: 'video 2' })
|
await uploadVideo(server.url, server.accessToken, { name: 'video 2' })
|
||||||
await waitJobs([ server ])
|
await waitJobs([ server ])
|
||||||
|
|
||||||
const res = await getLogs(server.url, server.accessToken, now)
|
const res = await getLogs(server.url, server.accessToken, now)
|
||||||
const logsString = JSON.stringify(res.body)
|
|
||||||
|
|
||||||
expect(logsString.includes('video 1')).to.be.false
|
|
||||||
expect(logsString.includes('video 2')).to.be.true
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Should get logs with an end date', async function () {
|
|
||||||
this.timeout(20000)
|
|
||||||
|
|
||||||
await uploadVideo(server.url, server.accessToken, { name: 'video 3' })
|
|
||||||
await waitJobs([ server ])
|
|
||||||
|
|
||||||
const now1 = new Date()
|
|
||||||
|
|
||||||
await uploadVideo(server.url, server.accessToken, { name: 'video 4' })
|
|
||||||
await waitJobs([ server ])
|
|
||||||
|
|
||||||
const now2 = new Date()
|
|
||||||
|
|
||||||
await uploadVideo(server.url, server.accessToken, { name: 'video 5' })
|
|
||||||
await waitJobs([ server ])
|
|
||||||
|
|
||||||
const res = await getLogs(server.url, server.accessToken, now1, now2)
|
|
||||||
const logsString = JSON.stringify(res.body)
|
|
||||||
|
|
||||||
expect(logsString.includes('video 3')).to.be.false
|
|
||||||
expect(logsString.includes('video 4')).to.be.true
|
|
||||||
expect(logsString.includes('video 5')).to.be.false
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Should get filter by level', async function () {
|
|
||||||
this.timeout(10000)
|
|
||||||
|
|
||||||
const now = new Date()
|
|
||||||
|
|
||||||
await uploadVideo(server.url, server.accessToken, { name: 'video 6' })
|
|
||||||
await waitJobs([ server ])
|
|
||||||
|
|
||||||
{
|
|
||||||
const res = await getLogs(server.url, server.accessToken, now, undefined, 'info')
|
|
||||||
const logsString = JSON.stringify(res.body)
|
const logsString = JSON.stringify(res.body)
|
||||||
|
|
||||||
expect(logsString.includes('video 6')).to.be.true
|
expect(logsString.includes('video 1')).to.be.false
|
||||||
}
|
expect(logsString.includes('video 2')).to.be.true
|
||||||
|
})
|
||||||
|
|
||||||
{
|
it('Should get logs with an end date', async function () {
|
||||||
const res = await getLogs(server.url, server.accessToken, now, undefined, 'warn')
|
this.timeout(20000)
|
||||||
|
|
||||||
|
await uploadVideo(server.url, server.accessToken, { name: 'video 3' })
|
||||||
|
await waitJobs([ server ])
|
||||||
|
|
||||||
|
const now1 = new Date()
|
||||||
|
|
||||||
|
await uploadVideo(server.url, server.accessToken, { name: 'video 4' })
|
||||||
|
await waitJobs([ server ])
|
||||||
|
|
||||||
|
const now2 = new Date()
|
||||||
|
|
||||||
|
await uploadVideo(server.url, server.accessToken, { name: 'video 5' })
|
||||||
|
await waitJobs([ server ])
|
||||||
|
|
||||||
|
const res = await getLogs(server.url, server.accessToken, now1, now2)
|
||||||
const logsString = JSON.stringify(res.body)
|
const logsString = JSON.stringify(res.body)
|
||||||
|
|
||||||
expect(logsString.includes('video 6')).to.be.false
|
expect(logsString.includes('video 3')).to.be.false
|
||||||
}
|
expect(logsString.includes('video 4')).to.be.true
|
||||||
|
expect(logsString.includes('video 5')).to.be.false
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should get filter by level', async function () {
|
||||||
|
this.timeout(10000)
|
||||||
|
|
||||||
|
const now = new Date()
|
||||||
|
|
||||||
|
await uploadVideo(server.url, server.accessToken, { name: 'video 6' })
|
||||||
|
await waitJobs([ server ])
|
||||||
|
|
||||||
|
{
|
||||||
|
const res = await getLogs(server.url, server.accessToken, now, undefined, 'info')
|
||||||
|
const logsString = JSON.stringify(res.body)
|
||||||
|
|
||||||
|
expect(logsString.includes('video 6')).to.be.true
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const res = await getLogs(server.url, server.accessToken, now, undefined, 'warn')
|
||||||
|
const logsString = JSON.stringify(res.body)
|
||||||
|
|
||||||
|
expect(logsString.includes('video 6')).to.be.false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('With the audit log', function () {
|
||||||
|
it('Should get logs with a start date', async function () {
|
||||||
|
this.timeout(10000)
|
||||||
|
|
||||||
|
await uploadVideo(server.url, server.accessToken, { name: 'video 7' })
|
||||||
|
await waitJobs([ server ])
|
||||||
|
|
||||||
|
const now = new Date()
|
||||||
|
|
||||||
|
await uploadVideo(server.url, server.accessToken, { name: 'video 8' })
|
||||||
|
await waitJobs([ server ])
|
||||||
|
|
||||||
|
const res = await getAuditLogs(server.url, server.accessToken, now)
|
||||||
|
const logsString = JSON.stringify(res.body)
|
||||||
|
|
||||||
|
expect(logsString.includes('video 7')).to.be.false
|
||||||
|
expect(logsString.includes('video 8')).to.be.true
|
||||||
|
|
||||||
|
expect(res.body).to.have.lengthOf(1)
|
||||||
|
|
||||||
|
const item = res.body[0]
|
||||||
|
|
||||||
|
const message = JSON.parse(item.message)
|
||||||
|
expect(message.domain).to.equal('videos')
|
||||||
|
expect(message.action).to.equal('create')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should get logs with an end date', async function () {
|
||||||
|
this.timeout(20000)
|
||||||
|
|
||||||
|
await uploadVideo(server.url, server.accessToken, { name: 'video 9' })
|
||||||
|
await waitJobs([ server ])
|
||||||
|
|
||||||
|
const now1 = new Date()
|
||||||
|
|
||||||
|
await uploadVideo(server.url, server.accessToken, { name: 'video 10' })
|
||||||
|
await waitJobs([ server ])
|
||||||
|
|
||||||
|
const now2 = new Date()
|
||||||
|
|
||||||
|
await uploadVideo(server.url, server.accessToken, { name: 'video 11' })
|
||||||
|
await waitJobs([ server ])
|
||||||
|
|
||||||
|
const res = await getAuditLogs(server.url, server.accessToken, now1, now2)
|
||||||
|
const logsString = JSON.stringify(res.body)
|
||||||
|
|
||||||
|
expect(logsString.includes('video 9')).to.be.false
|
||||||
|
expect(logsString.includes('video 10')).to.be.true
|
||||||
|
expect(logsString.includes('video 11')).to.be.false
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
after(async function () {
|
after(async function () {
|
||||||
|
|
|
@ -13,6 +13,19 @@ function getLogs (url: string, accessToken: string, startDate: Date, endDate?: D
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
function getAuditLogs (url: string, accessToken: string, startDate: Date, endDate?: Date) {
|
||||||
getLogs
|
const path = '/api/v1/server/audit-logs'
|
||||||
|
|
||||||
|
return makeGetRequest({
|
||||||
|
url,
|
||||||
|
path,
|
||||||
|
token: accessToken,
|
||||||
|
query: { startDate, endDate },
|
||||||
|
statusCodeExpected: 200
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
getLogs,
|
||||||
|
getAuditLogs
|
||||||
}
|
}
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
export type LogLevel = 'debug' | 'info' | 'warn' | 'error'
|
export type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'audit'
|
||||||
|
|
Loading…
Reference in New Issue