parse-log.ts 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161
  1. import { program } from 'commander'
  2. import { createReadStream } from 'fs'
  3. import { readdir } from 'fs/promises'
  4. import { join } from 'path'
  5. import { stdin } from 'process'
  6. import { createInterface } from 'readline'
  7. import { format as sqlFormat } from 'sql-formatter'
  8. import { inspect } from 'util'
  9. import * as winston from 'winston'
  10. import { labelFormatter, mtimeSortFilesDesc } from '@server/helpers/logger.js'
  11. import { CONFIG } from '@server/initializers/config.js'
  12. program
  13. .option('-l, --level [level]', 'Level log (debug/info/warn/error)')
  14. .option('-f, --files [file...]', 'Files to parse. If not provided, the script will parse the latest log file from config)')
  15. .option('-t, --tags [tags...]', 'Display only lines with these tags')
  16. .option('-nt, --not-tags [tags...]', 'Do not display lines containing these tags')
  17. .parse(process.argv)
  18. const options = program.opts()
  19. const excludedKeys = {
  20. level: true,
  21. message: true,
  22. splat: true,
  23. timestamp: true,
  24. tags: true,
  25. label: true,
  26. sql: true
  27. }
  28. function keysExcluder (key, value) {
  29. return excludedKeys[key] === true ? undefined : value
  30. }
  31. const loggerFormat = winston.format.printf((info) => {
  32. let additionalInfos = JSON.stringify(info, keysExcluder, 2)
  33. if (additionalInfos === '{}') additionalInfos = ''
  34. else additionalInfos = ' ' + additionalInfos
  35. if (info.sql) {
  36. if (CONFIG.LOG.PRETTIFY_SQL) {
  37. additionalInfos += '\n' + sqlFormat(info.sql, {
  38. language: 'sql',
  39. tabWidth: 2
  40. })
  41. } else {
  42. additionalInfos += ' - ' + info.sql
  43. }
  44. }
  45. return `[${info.label}] ${toTimeFormat(info.timestamp)} ${info.level}: ${info.message}${additionalInfos}`
  46. })
  47. const logger = winston.createLogger({
  48. transports: [
  49. new winston.transports.Console({
  50. level: options.level || 'debug',
  51. stderrLevels: [],
  52. format: winston.format.combine(
  53. winston.format.splat(),
  54. labelFormatter(),
  55. winston.format.colorize(),
  56. loggerFormat
  57. )
  58. })
  59. ],
  60. exitOnError: true
  61. })
  62. const logLevels = {
  63. error: logger.error.bind(logger),
  64. warn: logger.warn.bind(logger),
  65. info: logger.info.bind(logger),
  66. debug: logger.debug.bind(logger)
  67. }
  68. run()
  69. .then(() => process.exit(0))
  70. .catch(err => console.error(err))
  71. async function run () {
  72. const files = await getFiles()
  73. for (const file of files) {
  74. if (file === 'peertube-audit.log') continue
  75. await readFile(file)
  76. }
  77. }
  78. function readFile (file: string) {
  79. console.log('Opening %s.', file)
  80. const stream = file === '-' ? stdin : createReadStream(file)
  81. const rl = createInterface({
  82. input: stream
  83. })
  84. return new Promise<void>(res => {
  85. rl.on('line', line => {
  86. try {
  87. const log = JSON.parse(line)
  88. if (options.tags && !containsTags(log.tags, options.tags)) {
  89. return
  90. }
  91. if (options.notTags && containsTags(log.tags, options.notTags)) {
  92. return
  93. }
  94. // Don't know why but loggerFormat does not remove splat key
  95. Object.assign(log, { splat: undefined })
  96. logLevels[log.level](log)
  97. } catch (err) {
  98. console.error('Cannot parse line.', inspect(line))
  99. throw err
  100. }
  101. })
  102. stream.once('end', () => res())
  103. })
  104. }
  105. // Thanks: https://stackoverflow.com/a/37014317
  106. async function getNewestFile (files: string[], basePath: string) {
  107. const sorted = await mtimeSortFilesDesc(files, basePath)
  108. return (sorted.length > 0) ? sorted[0].file : ''
  109. }
  110. async function getFiles () {
  111. if (options.files) return options.files
  112. const logFiles = await readdir(CONFIG.STORAGE.LOG_DIR)
  113. const filename = await getNewestFile(logFiles, CONFIG.STORAGE.LOG_DIR)
  114. return [ join(CONFIG.STORAGE.LOG_DIR, filename) ]
  115. }
  116. function toTimeFormat (time: string) {
  117. const timestamp = Date.parse(time)
  118. if (isNaN(timestamp) === true) return 'Unknown date'
  119. const d = new Date(timestamp)
  120. return d.toLocaleString() + `.${d.getMilliseconds()}`
  121. }
  122. function containsTags (loggerTags: string[], optionsTags: string[]) {
  123. if (!loggerTags) return false
  124. for (const lt of loggerTags) {
  125. for (const ot of optionsTags) {
  126. if (lt === ot) return true
  127. }
  128. }
  129. return false
  130. }