1 | const path = require('path')
|
2 | const util = require('util')
|
3 | const getConfigDir = require('./config/getConfigDir')
|
4 | const strip = require('strip-ansi')
|
5 | const chalk = require('chalk')
|
6 | const fs = require('fs-extra')
|
7 | const gzip = require('./gzip')
|
8 |
|
9 | const timezonedDate = formatter => {
|
10 | let date = new Date()
|
11 | date.setMinutes(date.getMinutes() + getOffset())
|
12 | return formatter(date.toJSON()).split('.')[0]
|
13 | }
|
14 |
|
15 | const isValidTimeZone = tz => {
|
16 | try {
|
17 | Intl.DateTimeFormat(undefined, { timeZone: tz })
|
18 | return true
|
19 | } catch (ex) {
|
20 | return false
|
21 | }
|
22 | }
|
23 | const getOffset = _ => {
|
24 | let offset = -(new Date()).getTimezoneOffset()
|
25 | if (global.config && config.timezone && isValidTimeZone(config.timezone)) {
|
26 |
|
27 | let date = new Date()
|
28 | let arr = date.toLocaleString('ja', { timeZone: config.timezone }).split(/[/\s:]/)
|
29 | arr[1]--
|
30 | return ((Date.UTC.apply(null, arr) - new Date(date).setMilliseconds(0)) / 60 / 1000) || offset
|
31 | }
|
32 | return offset
|
33 | }
|
34 | const getWritableDate = () => timezonedDate(d => d.replace('T', '_').replace(/:/g, '-'))
|
35 |
|
36 | const levelBadges = {
|
37 | fatal: chalk.bgWhite.red(' FATAL '),
|
38 | error: chalk.bgRed.white(' ERROR '),
|
39 | warn: chalk.bgYellow.black(' WARN '),
|
40 | info: chalk.bgBlue.black(' INFO '),
|
41 | start: chalk.bgBlue.black(' START '),
|
42 | success: chalk.bgGreen.black(' SUCCESS '),
|
43 | debug: chalk.bgCyan.black(' DEBUG '),
|
44 | trace: chalk.bgWhite.black(' TRACE ')
|
45 | }
|
46 |
|
47 | const levelArrows = {
|
48 | fatal: chalk.white('›'),
|
49 | error: chalk.red('›'),
|
50 | warn: chalk.yellow('›'),
|
51 | info: chalk.blue('›'),
|
52 | start: chalk.green('›'),
|
53 | success: chalk.green('›'),
|
54 | debug: chalk.cyan('›'),
|
55 | trace: chalk.white('›')
|
56 | }
|
57 |
|
58 | const levels = {
|
59 | silent: -1,
|
60 |
|
61 | fatal: 0,
|
62 | error: 0,
|
63 |
|
64 | warn: 1,
|
65 |
|
66 | info: 2,
|
67 | start: 2,
|
68 | success: 2,
|
69 |
|
70 | verbose: 3,
|
71 | debug: 3,
|
72 |
|
73 | silly: 4,
|
74 | trace: 4
|
75 | }
|
76 |
|
77 | class Logger {
|
78 | constructor (level, scope) {
|
79 | this.level = typeof level === 'number' ? level : levels[level] || 'info'
|
80 | if (scope) {
|
81 | this.scope = scope
|
82 | } else {
|
83 | this.scopedLoggers = []
|
84 | }
|
85 | }
|
86 |
|
87 | withScope (scope) {
|
88 | if (this.scope) throw new Error('Cannot create scoped logger from a scoped logger!')
|
89 | const logger = new Logger(this.level, scope)
|
90 | this.scopedLoggers.push(logger)
|
91 | return logger
|
92 | }
|
93 |
|
94 | setLevel (level) {
|
95 | this.level = levels[level]
|
96 | if (!this.scope) this.scopedLoggers.forEach(logger => logger.setLevel(level))
|
97 | }
|
98 |
|
99 | getScopeForLevel (level) {
|
100 | return (this.scope || '')
|
101 | .split(':')
|
102 | .map(frag => chalk.reset(frag))
|
103 | .join(` ${levelArrows[level]} `)
|
104 | }
|
105 |
|
106 | inspect (object, depth = 2) {
|
107 | return util.inspect(object, { depth, colors: true, breakLength: (process.stdout.columns - 20) })
|
108 | }
|
109 |
|
110 | log (logLevel, msg, obj, depth) {
|
111 | if (this.level < levels[logLevel]) return
|
112 | if (typeof msg === 'object') {
|
113 | msg = [
|
114 | chalk.reset(),
|
115 | this.inspect(msg, obj || 2)
|
116 | ]
|
117 | } else {
|
118 | msg = [
|
119 | chalk.reset(msg),
|
120 | obj ? (
|
121 | (typeof obj === 'string' ? '' : '\n') + this.inspect(obj, depth || 2)
|
122 | ) : ''
|
123 | ]
|
124 | }
|
125 | console.log(
|
126 | levelBadges[logLevel],
|
127 | chalk.gray(timezonedDate(d => d.replace('T', ' '))),
|
128 | this.getScopeForLevel(logLevel),
|
129 | levelArrows[logLevel],
|
130 | ...msg
|
131 | )
|
132 | }
|
133 |
|
134 | fatal () { this.log('fatal', ...arguments) }
|
135 | error () { this.log('error', ...arguments) }
|
136 | info () { this.log('info', ...arguments) }
|
137 | start () { this.log('start', ...arguments) }
|
138 | success () { this.log('success', ...arguments) }
|
139 | warn () { this.log('warn', ...arguments) }
|
140 | debug () { this.log('debug', ...arguments) }
|
141 | trace () { this.log('trace', ...arguments) }
|
142 | }
|
143 |
|
144 | module.exports = Logger
|
145 |
|
146 | module.exports.gzipOldLogs = async (configPath = getConfigDir()) => {
|
147 | let files
|
148 | try {
|
149 | files = await fs.readdir(path.join(configPath, 'logs'))
|
150 | } catch (err) {
|
151 | return
|
152 | }
|
153 | await files
|
154 | .filter(file => file.endsWith('.log'))
|
155 | .map(file => path.join(configPath, 'logs', file))
|
156 | .reduce((promise, item) => promise.then(() => gzip(item)), Promise.resolve())
|
157 | }
|
158 |
|
159 | module.exports.inject = (configPath = getConfigDir()) => {
|
160 | const dir = path.join(configPath, 'logs')
|
161 | fs.ensureDirSync(dir)
|
162 |
|
163 | const filename = getWritableDate() + '.log'
|
164 | const logStream = fs.createWriteStream(path.join(dir, filename))
|
165 |
|
166 | let line = ''
|
167 |
|
168 |
|
169 | function bindWrite (write, stream) {
|
170 | return (string, encoding, fd) => {
|
171 | if (string === 'close\n') return logStream.end(() => process.exit(1))
|
172 | line += string
|
173 | if (!string.includes('\n')) return
|
174 | write.call(process[stream], line, encoding, fd)
|
175 | logStream.write(strip(line), encoding, fd)
|
176 | line = ''
|
177 | }
|
178 | }
|
179 | process.stdout.write = bindWrite(process.stdout.write, 'stdout')
|
180 | process.stderr.write = bindWrite(process.stderr.write, 'stderr')
|
181 | }
|