UNPKG

7.44 kBJavaScriptView Raw
1const {default: ux} = require('cli-ux')
2const {default: c} = require('@heroku-cli/color')
3
4const COLORS = [
5 s => c.yellow(s),
6 s => c.green(s),
7 s => c.cyan(s),
8 s => c.magenta(s),
9 s => c.blue(s),
10 s => c.bold.green(s),
11 s => c.bold.cyan(s),
12 s => c.bold.magenta(s),
13 s => c.bold.yellow(s),
14 s => c.bold.blue(s)
15]
16const assignedColors = {}
17function getColorForIdentifier (i) {
18 i = i.split('.')[0]
19 if (assignedColors[i]) return assignedColors[i]
20 assignedColors[i] = COLORS[Object.keys(assignedColors).length % COLORS.length]
21 return assignedColors[i]
22}
23
24// get initial colors so they are the same every time
25getColorForIdentifier('run')
26getColorForIdentifier('router')
27getColorForIdentifier('web')
28getColorForIdentifier('postgres')
29getColorForIdentifier('heroku-postgres')
30
31let lineRegex = /^(.*?\[([\w-]+)([\d.]+)?]:)(.*)?$/
32
33const red = c.red
34const dim = i => c.dim(i)
35const other = dim
36const path = i => c.blue(i)
37const method = i => c.bold.magenta(i)
38const status = code => {
39 if (code < 200) return code
40 if (code < 300) return c.green(code)
41 if (code < 400) return c.cyan(code)
42 if (code < 500) return c.yellow(code)
43 if (code < 600) return c.red(code)
44 return code
45}
46const ms = s => {
47 const ms = parseInt(s)
48 if (!ms) return s
49 if (ms < 100) return c.greenBright(s)
50 if (ms < 500) return c.green(s)
51 if (ms < 5000) return c.yellow(s)
52 if (ms < 10000) return c.yellowBright(s)
53 return c.red(s)
54}
55
56function colorizeRouter (body) {
57 try {
58 const tokens = body.split('=')
59 let cur = tokens[0]
60 const obj = {}
61 for (let i = 1; i < tokens.length; i++) {
62 obj[cur] = tokens[i].split(' ').slice(0, -1).join(' ')
63 let next = tokens[i].split(' ').pop()
64 if (i + 1 === tokens.length) obj[cur] = next
65 cur = next
66 }
67 if (Object.keys(obj).length === 0) return body
68 return Object.entries(obj)
69 .map(([k, v]) => {
70 switch (k) {
71 case 'at': return [k, v === 'error' ? red(v) : other(v)]
72 case 'desc': return [k, red(v)]
73 case 'code': return [k, red.bold(v)]
74 case 'method': return [k, method(v)]
75 case 'dyno': return [k, getColorForIdentifier(v)(v)]
76 case 'status': return [k, status(v)]
77 case 'path': return [k, path(v)]
78 case 'connect': return [k, ms(v)]
79 case 'service': return [k, ms(v)]
80 default: return [k, other(v)]
81 }
82 })
83 .map(([k, v]) => other(k + '=') + v)
84 .join(' ')
85 } catch (err) {
86 ux.warn(err)
87 return body
88 }
89}
90
91const state = s => {
92 switch (s) {
93 case 'down': return red(s)
94 case 'up': return c.greenBright(s)
95 case 'starting': return c.yellowBright(s)
96 case 'complete': return c.greenBright(s)
97 default: return s
98 }
99}
100
101function colorizeRun (body) {
102 try {
103 if (body.match(/^Stopping all processes with SIGTERM$/)) return c.red(body)
104 let starting = body.match(/^(Starting process with command )(`.+`)(by user )?(.*)?$/)
105 if (starting) {
106 return [
107 starting[1],
108 c.cmd(starting[2]),
109 starting[3] || '',
110 c.green(starting[4] || '')
111 ].join('')
112 }
113 let stateChange = body.match(/^(State changed from )(\w+)( to )(\w+)$/)
114 if (stateChange) {
115 return [
116 stateChange[1],
117 state(stateChange[2]),
118 stateChange[3] || '',
119 state(stateChange[4] || '')
120 ].join('')
121 }
122 let exited = body.match(/^(Process exited with status )(\d+)$/)
123 if (exited) {
124 return [
125 exited[1],
126 exited[2] === '0' ? c.greenBright(exited[2]) : c.red(exited[2])
127 ].join('')
128 }
129 } catch (err) {
130 ux.warn(err)
131 }
132 return body
133}
134
135function colorizeWeb (body) {
136 try {
137 if (body.match(/^Unidling$/)) return c.yellow(body)
138 if (body.match(/^Restarting$/)) return c.yellow(body)
139 if (body.match(/^Stopping all processes with SIGTERM$/)) return c.red(body)
140 let starting = body.match(/^(Starting process with command )(`.+`)(by user )?(.*)?$/)
141 if (starting) {
142 return [
143 (starting[1]),
144 c.cmd(starting[2]),
145 (starting[3] || ''),
146 c.green(starting[4] || '')
147 ].join('')
148 }
149 let exited = body.match(/^(Process exited with status )(\d+)$/)
150 if (exited) {
151 return [
152 exited[1],
153 exited[2] === '0' ? c.greenBright(exited[2]) : c.red(exited[2])
154 ].join('')
155 }
156 let stateChange = body.match(/^(State changed from )(\w+)( to )(\w+)$/)
157 if (stateChange) {
158 return [
159 stateChange[1],
160 state(stateChange[2]),
161 stateChange[3],
162 state(stateChange[4])
163 ].join('')
164 }
165 let apache = body.match(/^(\d+\.\d+\.\d+\.\d+ -[^-]*- \[[^\]]+\] ")(\w+)( )([^ ]+)( HTTP\/\d+\.\d+" )(\d+)( .+$)/)
166 if (apache) {
167 const [, ...tokens] = apache
168 return [
169 other(tokens[0]),
170 method(tokens[1]),
171 other(tokens[2]),
172 path(tokens[3]),
173 other(tokens[4]),
174 status(tokens[5]),
175 other(tokens[6])
176 ].join('')
177 }
178 let route = body.match(/^(.* ")(\w+)(.+)(HTTP\/\d+\.\d+" .*)$/)
179 if (route) {
180 return [
181 route[1],
182 method(route[2]),
183 path(route[3]),
184 route[4]
185 ].join('')
186 }
187 } catch (err) {
188 ux.warn(err)
189 }
190 return body
191}
192
193function colorizeAPI (body) {
194 if (body.match(/^Build succeeded$/)) return c.greenBright(body)
195 if (body.match(/^Build failed/)) return c.red(body)
196 const build = body.match(/^(Build started by user )(.+)$/)
197 if (build) {
198 return [
199 build[1],
200 c.green(build[2])
201 ].join('')
202 }
203 const deploy = body.match(/^(Deploy )([\w]+)( by user )(.+)$/)
204 if (deploy) {
205 return [
206 deploy[1],
207 c.cyan(deploy[2]),
208 deploy[3],
209 c.green(deploy[4])
210 ].join('')
211 }
212 const release = body.match(/^(Release )(v[\d]+)( created by user )(.+)$/)
213 if (release) {
214 return [
215 release[1],
216 c.magenta(release[2]),
217 release[3],
218 c.green(release[4])
219 ].join('')
220 }
221 let starting = body.match(/^(Starting process with command )(`.+`)(by user )?(.*)?$/)
222 if (starting) {
223 return [
224 (starting[1]),
225 c.cmd(starting[2]),
226 (starting[3] || ''),
227 c.green(starting[4] || '')
228 ].join('')
229 }
230 return body
231}
232
233function colorizeRedis (body) {
234 if (body.match(/source=\w+ sample#/)) {
235 body = dim(body)
236 }
237 return body
238}
239
240function colorizePG (body) {
241 let create = body.match(/^(\[DATABASE\].*)(CREATE TABLE)(.*)$/)
242 if (create) {
243 return [
244 other(create[1]),
245 c.magenta(create[2]),
246 c.cyan(create[3])
247 ].join('')
248 }
249 if (body.match(/source=\w+ sample#/)) {
250 body = dim(body)
251 }
252 return body
253}
254
255module.exports = function colorize (line) {
256 if (process.env.HEROKU_LOGS_COLOR === '0') return line
257
258 let parsed = line.match(lineRegex)
259 if (!parsed) return line
260 let header = parsed[1]
261 let identifier = parsed[2]
262 let body = (parsed[4] || '').trim()
263 switch (identifier) {
264 case 'api':
265 body = colorizeAPI(body)
266 break
267 case 'router':
268 body = colorizeRouter(body)
269 break
270 case 'run':
271 body = colorizeRun(body)
272 break
273 case 'web':
274 body = colorizeWeb(body)
275 break
276 case 'heroku-redis':
277 body = colorizeRedis(body)
278 break
279 case 'heroku-postgres':
280 case 'postgres':
281 body = colorizePG(body)
282 }
283 return getColorForIdentifier(identifier)(header) + ' ' + body
284}
285
286module.exports.COLORS = COLORS