diff options
Diffstat (limited to 'server/scripts/parse-log.ts')
-rwxr-xr-x | server/scripts/parse-log.ts | 161 |
1 files changed, 161 insertions, 0 deletions
diff --git a/server/scripts/parse-log.ts b/server/scripts/parse-log.ts new file mode 100755 index 000000000..e80c0d927 --- /dev/null +++ b/server/scripts/parse-log.ts | |||
@@ -0,0 +1,161 @@ | |||
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 | |||
13 | program | ||
14 | .option('-l, --level [level]', 'Level log (debug/info/warn/error)') | ||
15 | .option('-f, --files [file...]', 'Files to parse. If not provided, the script will parse the latest log file from config)') | ||
16 | .option('-t, --tags [tags...]', 'Display only lines with these tags') | ||
17 | .option('-nt, --not-tags [tags...]', 'Donrt display lines containing these tags') | ||
18 | .parse(process.argv) | ||
19 | |||
20 | const options = program.opts() | ||
21 | |||
22 | const excludedKeys = { | ||
23 | level: true, | ||
24 | message: true, | ||
25 | splat: true, | ||
26 | timestamp: true, | ||
27 | tags: true, | ||
28 | label: true, | ||
29 | sql: true | ||
30 | } | ||
31 | function keysExcluder (key, value) { | ||
32 | return excludedKeys[key] === true ? undefined : value | ||
33 | } | ||
34 | |||
35 | const loggerFormat = winston.format.printf((info) => { | ||
36 | let additionalInfos = JSON.stringify(info, keysExcluder, 2) | ||
37 | if (additionalInfos === '{}') additionalInfos = '' | ||
38 | else additionalInfos = ' ' + additionalInfos | ||
39 | |||
40 | if (info.sql) { | ||
41 | if (CONFIG.LOG.PRETTIFY_SQL) { | ||
42 | additionalInfos += '\n' + sqlFormat(info.sql, { | ||
43 | language: 'sql', | ||
44 | tabWidth: 2 | ||
45 | }) | ||
46 | } else { | ||
47 | additionalInfos += ' - ' + info.sql | ||
48 | } | ||
49 | } | ||
50 | |||
51 | return `[${info.label}] ${toTimeFormat(info.timestamp)} ${info.level}: ${info.message}${additionalInfos}` | ||
52 | }) | ||
53 | |||
54 | const logger = winston.createLogger({ | ||
55 | transports: [ | ||
56 | new winston.transports.Console({ | ||
57 | level: options.level || 'debug', | ||
58 | stderrLevels: [], | ||
59 | format: winston.format.combine( | ||
60 | winston.format.splat(), | ||
61 | labelFormatter(), | ||
62 | winston.format.colorize(), | ||
63 | loggerFormat | ||
64 | ) | ||
65 | }) | ||
66 | ], | ||
67 | exitOnError: true | ||
68 | }) | ||
69 | |||
70 | const logLevels = { | ||
71 | error: logger.error.bind(logger), | ||
72 | warn: logger.warn.bind(logger), | ||
73 | info: logger.info.bind(logger), | ||
74 | debug: logger.debug.bind(logger) | ||
75 | } | ||
76 | |||
77 | run() | ||
78 | .then(() => process.exit(0)) | ||
79 | .catch(err => console.error(err)) | ||
80 | |||
81 | async function run () { | ||
82 | const files = await getFiles() | ||
83 | |||
84 | for (const file of files) { | ||
85 | if (file === 'peertube-audit.log') continue | ||
86 | |||
87 | await readFile(file) | ||
88 | } | ||
89 | } | ||
90 | |||
91 | function readFile (file: string) { | ||
92 | console.log('Opening %s.', file) | ||
93 | |||
94 | const stream = file === '-' ? stdin : createReadStream(file) | ||
95 | |||
96 | const rl = createInterface({ | ||
97 | input: stream | ||
98 | }) | ||
99 | |||
100 | return new Promise<void>(res => { | ||
101 | rl.on('line', line => { | ||
102 | try { | ||
103 | const log = JSON.parse(line) | ||
104 | if (options.tags && !containsTags(log.tags, options.tags)) { | ||
105 | return | ||
106 | } | ||
107 | |||
108 | if (options.notTags && containsTags(log.tags, options.notTags)) { | ||
109 | return | ||
110 | } | ||
111 | |||
112 | // Don't know why but loggerFormat does not remove splat key | ||
113 | Object.assign(log, { splat: undefined }) | ||
114 | |||
115 | logLevels[log.level](log) | ||
116 | } catch (err) { | ||
117 | console.error('Cannot parse line.', inspect(line)) | ||
118 | throw err | ||
119 | } | ||
120 | }) | ||
121 | |||
122 | stream.once('end', () => res()) | ||
123 | }) | ||
124 | } | ||
125 | |||
126 | // Thanks: https://stackoverflow.com/a/37014317 | ||
127 | async function getNewestFile (files: string[], basePath: string) { | ||
128 | const sorted = await mtimeSortFilesDesc(files, basePath) | ||
129 | |||
130 | return (sorted.length > 0) ? sorted[0].file : '' | ||
131 | } | ||
132 | |||
133 | async function getFiles () { | ||
134 | if (options.files) return options.files | ||
135 | |||
136 | const logFiles = await readdir(CONFIG.STORAGE.LOG_DIR) | ||
137 | |||
138 | const filename = await getNewestFile(logFiles, CONFIG.STORAGE.LOG_DIR) | ||
139 | return [ join(CONFIG.STORAGE.LOG_DIR, filename) ] | ||
140 | } | ||
141 | |||
142 | function toTimeFormat (time: string) { | ||
143 | const timestamp = Date.parse(time) | ||
144 | |||
145 | if (isNaN(timestamp) === true) return 'Unknown date' | ||
146 | |||
147 | const d = new Date(timestamp) | ||
148 | return d.toLocaleString() + `.${d.getMilliseconds()}` | ||
149 | } | ||
150 | |||
151 | function containsTags (loggerTags: string[], optionsTags: string[]) { | ||
152 | if (!loggerTags) return false | ||
153 | |||
154 | for (const lt of loggerTags) { | ||
155 | for (const ot of optionsTags) { | ||
156 | if (lt === ot) return true | ||
157 | } | ||
158 | } | ||
159 | |||
160 | return false | ||
161 | } | ||