UNPKG

7.37 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 const encodeColor = ([k, v]) => {
58 switch (k) {
59 case 'at': return [k, v === 'error' ? red(v) : other(v)]
60 case 'code': return [k, red.bold(v)]
61 case 'method': return [k, method(v)]
62 case 'dyno': return [k, getColorForIdentifier(v)(v)]
63 case 'status': return [k, status(v)]
64 case 'path': return [k, path(v)]
65 case 'connect': return [k, ms(v)]
66 case 'service': return [k, ms(v)]
67 default: return [k, other(v)]
68 }
69 }
70
71 try {
72 const tokens = body.split(/\s+/).map((sub) => {
73 const parts = sub.split('=')
74 if (parts.length === 1) {
75 return parts
76 } else if (parts.length === 2) {
77 return encodeColor(parts)
78 } else {
79 return encodeColor([parts[0], parts.splice(1).join("=")])
80 }
81 })
82
83 return tokens.map(([k, v]) => {
84 if (v === undefined) {
85 return other(k)
86 }
87 return other(k + '=') + v
88 }).join(' ')
89 } catch (err) {
90 ux.warn(err)
91 return body
92 }
93}
94
95const state = s => {
96 switch (s) {
97 case 'down': return red(s)
98 case 'up': return c.greenBright(s)
99 case 'starting': return c.yellowBright(s)
100 case 'complete': return c.greenBright(s)
101 default: return s
102 }
103}
104
105function colorizeRun (body) {
106 try {
107 if (body.match(/^Stopping all processes with SIGTERM$/)) return c.red(body)
108 let starting = body.match(/^(Starting process with command )(`.+`)(by user )?(.*)?$/)
109 if (starting) {
110 return [
111 starting[1],
112 c.cmd(starting[2]),
113 starting[3] || '',
114 c.green(starting[4] || '')
115 ].join('')
116 }
117 let stateChange = body.match(/^(State changed from )(\w+)( to )(\w+)$/)
118 if (stateChange) {
119 return [
120 stateChange[1],
121 state(stateChange[2]),
122 stateChange[3] || '',
123 state(stateChange[4] || '')
124 ].join('')
125 }
126 let exited = body.match(/^(Process exited with status )(\d+)$/)
127 if (exited) {
128 return [
129 exited[1],
130 exited[2] === '0' ? c.greenBright(exited[2]) : c.red(exited[2])
131 ].join('')
132 }
133 } catch (err) {
134 ux.warn(err)
135 }
136 return body
137}
138
139function colorizeWeb (body) {
140 try {
141 if (body.match(/^Unidling$/)) return c.yellow(body)
142 if (body.match(/^Restarting$/)) return c.yellow(body)
143 if (body.match(/^Stopping all processes with SIGTERM$/)) return c.red(body)
144 let starting = body.match(/^(Starting process with command )(`.+`)(by user )?(.*)?$/)
145 if (starting) {
146 return [
147 (starting[1]),
148 c.cmd(starting[2]),
149 (starting[3] || ''),
150 c.green(starting[4] || '')
151 ].join('')
152 }
153 let exited = body.match(/^(Process exited with status )(\d+)$/)
154 if (exited) {
155 return [
156 exited[1],
157 exited[2] === '0' ? c.greenBright(exited[2]) : c.red(exited[2])
158 ].join('')
159 }
160 let stateChange = body.match(/^(State changed from )(\w+)( to )(\w+)$/)
161 if (stateChange) {
162 return [
163 stateChange[1],
164 state(stateChange[2]),
165 stateChange[3],
166 state(stateChange[4])
167 ].join('')
168 }
169 let apache = body.match(/^(\d+\.\d+\.\d+\.\d+ -[^-]*- \[[^\]]+] ")(\w+)( )([^ ]+)( HTTP\/\d+\.\d+" )(\d+)( .+$)/)
170 if (apache) {
171 const [, ...tokens] = apache
172 return [
173 other(tokens[0]),
174 method(tokens[1]),
175 other(tokens[2]),
176 path(tokens[3]),
177 other(tokens[4]),
178 status(tokens[5]),
179 other(tokens[6])
180 ].join('')
181 }
182 let route = body.match(/^(.* ")(\w+)(.+)(HTTP\/\d+\.\d+" .*)$/)
183 if (route) {
184 return [
185 route[1],
186 method(route[2]),
187 path(route[3]),
188 route[4]
189 ].join('')
190 }
191 } catch (err) {
192 ux.warn(err)
193 }
194 return body
195}
196
197function colorizeAPI (body) {
198 if (body.match(/^Build succeeded$/)) return c.greenBright(body)
199 if (body.match(/^Build failed/)) return c.red(body)
200 const build = body.match(/^(Build started by user )(.+)$/)
201 if (build) {
202 return [
203 build[1],
204 c.green(build[2])
205 ].join('')
206 }
207 const deploy = body.match(/^(Deploy )([\w]+)( by user )(.+)$/)
208 if (deploy) {
209 return [
210 deploy[1],
211 c.cyan(deploy[2]),
212 deploy[3],
213 c.green(deploy[4])
214 ].join('')
215 }
216 const release = body.match(/^(Release )(v[\d]+)( created by user )(.+)$/)
217 if (release) {
218 return [
219 release[1],
220 c.magenta(release[2]),
221 release[3],
222 c.green(release[4])
223 ].join('')
224 }
225 let starting = body.match(/^(Starting process with command )(`.+`)(by user )?(.*)?$/)
226 if (starting) {
227 return [
228 (starting[1]),
229 c.cmd(starting[2]),
230 (starting[3] || ''),
231 c.green(starting[4] || '')
232 ].join('')
233 }
234 return body
235}
236
237function colorizeRedis (body) {
238 if (body.match(/source=\w+ sample#/)) {
239 body = dim(body)
240 }
241 return body
242}
243
244function colorizePG (body) {
245 let create = body.match(/^(\[DATABASE].*)(CREATE TABLE)(.*)$/)
246 if (create) {
247 return [
248 other(create[1]),
249 c.magenta(create[2]),
250 c.cyan(create[3])
251 ].join('')
252 }
253 if (body.match(/source=\w+ sample#/)) {
254 body = dim(body)
255 }
256 return body
257}
258
259module.exports = function colorize (line) {
260 if (process.env.HEROKU_LOGS_COLOR === '0') return line
261
262 let parsed = line.match(lineRegex)
263 if (!parsed) return line
264 let header = parsed[1]
265 let identifier = parsed[2]
266 let body = (parsed[4] || '').trim()
267 switch (identifier) {
268 case 'api':
269 body = colorizeAPI(body)
270 break
271 case 'router':
272 body = colorizeRouter(body)
273 break
274 case 'run':
275 body = colorizeRun(body)
276 break
277 case 'web':
278 body = colorizeWeb(body)
279 break
280 case 'heroku-redis':
281 body = colorizeRedis(body)
282 break
283 case 'heroku-postgres':
284 case 'postgres':
285 body = colorizePG(body)
286 }
287 return getColorForIdentifier(identifier)(header) + ' ' + body
288}
289
290module.exports.COLORS = COLORS