]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blobdiff - server/controllers/api/server/logs.ts
Add ability for client to create server logs
[github/Chocobozzz/PeerTube.git] / server / controllers / api / server / logs.ts
index e9d1f2efdeca013095f879dd4f195ffd1628af4d..ed0aa6e8e78f1435649afab62ebf32768bcc4943 100644 (file)
@@ -1,16 +1,31 @@
-import * as express from 'express'
-import { UserRight } from '../../../../shared/models/users'
-import { asyncMiddleware, authenticate, ensureUserHasRight } from '../../../middlewares'
-import { mtimeSortFilesDesc } from '../../../../shared/core-utils/logs/logs'
+import express from 'express'
 import { readdir, readFile } from 'fs-extra'
-import { MAX_LOGS_OUTPUT_CHARACTERS } from '../../../initializers/constants'
 import { join } from 'path'
-import { getLogsValidator } from '../../../middlewares/validators/logs'
-import { LogLevel } from '../../../../shared/models/server/log-level.type'
+import { isArray } from '@server/helpers/custom-validators/misc'
+import { logger, mtimeSortFilesDesc } from '@server/helpers/logger'
+import { pick } from '@shared/core-utils'
+import { ClientLogCreate, HttpStatusCode } from '@shared/models'
+import { ServerLogLevel } from '../../../../shared/models/server/server-log-level.type'
+import { UserRight } from '../../../../shared/models/users'
 import { CONFIG } from '../../../initializers/config'
+import { AUDIT_LOG_FILENAME, LOG_FILENAME, MAX_LOGS_OUTPUT_CHARACTERS } from '../../../initializers/constants'
+import { asyncMiddleware, authenticate, buildRateLimiter, ensureUserHasRight, optionalAuthenticate } from '../../../middlewares'
+import { createClientLogValidator, getAuditLogsValidator, getLogsValidator } from '../../../middlewares/validators/logs'
+
+const createClientLogRateLimiter = buildRateLimiter({
+  windowMs: CONFIG.RATES_LIMIT.RECEIVE_CLIENT_LOG.WINDOW_MS,
+  max: CONFIG.RATES_LIMIT.RECEIVE_CLIENT_LOG.MAX
+})
 
 const logsRouter = express.Router()
 
+logsRouter.post('/logs/client',
+  createClientLogRateLimiter,
+  optionalAuthenticate,
+  createClientLogValidator,
+  createClientLog
+)
+
 logsRouter.get('/logs',
   authenticate,
   ensureUserHasRight(UserRight.MANAGE_LOGS),
@@ -18,6 +33,13 @@ logsRouter.get('/logs',
   asyncMiddleware(getLogs)
 )
 
+logsRouter.get('/audit-logs',
+  authenticate,
+  ensureUserHasRight(UserRight.MANAGE_LOGS),
+  getAuditLogsValidator,
+  asyncMiddleware(getAuditLogs)
+)
+
 // ---------------------------------------------------------------------------
 
 export {
@@ -26,21 +48,76 @@ export {
 
 // ---------------------------------------------------------------------------
 
+function createClientLog (req: express.Request, res: express.Response) {
+  const logInfo = req.body as ClientLogCreate
+
+  const meta = {
+    tags: [ 'client' ],
+    username: res.locals.oauth?.token?.User?.username,
+
+    ...pick(logInfo, [ 'userAgent', 'stackTrace', 'meta', 'url' ])
+  }
+
+  logger.log(logInfo.level, `Client log: ${logInfo.message}`, meta)
+
+  return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
+}
+
+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) {
+  const output = await generateOutput({
+    startDateQuery: req.query.startDate,
+    endDateQuery: req.query.endDate,
+    level: req.query.level || 'info',
+    tagsOneOf: req.query.tagsOneOf,
+    nameFilter: logNameFilter
+  })
+
+  return res.json(output)
+}
+
+async function generateOutput (options: {
+  startDateQuery: string
+  endDateQuery?: string
+
+  level: ServerLogLevel
+  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 startDate = new Date(req.query.startDate)
-  const endDate = req.query.endDate ? new Date(req.query.endDate) : new Date()
-  const level: LogLevel = req.query.level || 'info'
+  const startDate = new Date(startDateQuery)
+  const endDate = options.endDateQuery ? new Date(options.endDateQuery) : new Date()
 
   let output: string[] = []
 
   for (const meta of sortedLogFiles) {
+    if (nameFilter.exec(meta.file) === null) continue
+
     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)
@@ -49,15 +126,27 @@ async function getLogs (req: express.Request, res: express.Response) {
     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 (options: {
+  path: string
+  startDate: Date
+  endDate: Date
+  level: ServerLogLevel
+  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 } = {
+  const logsLevel: { [ id in ServerLogLevel ]: number } = {
+    audit: -1,
     debug: 0,
     info: 1,
     warn: 2,
@@ -69,7 +158,7 @@ async function getOutputFromFile (path: string, startDate: Date, endDate: Date,
   const output: any[] = []
 
   for (let i = lines.length - 1; i >= 0; i--) {
-    const line = lines[ i ]
+    const line = lines[i]
     let log: any
 
     try {
@@ -80,7 +169,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
@@ -93,3 +187,17 @@ 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$')
+}