1 | const {default: ux} = require('cli-ux')
|
2 | const {default: c} = require('@heroku-cli/color')
|
3 |
|
4 | const 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 | ]
|
16 | const assignedColors = {}
|
17 | function 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 |
|
25 | getColorForIdentifier('run')
|
26 | getColorForIdentifier('router')
|
27 | getColorForIdentifier('web')
|
28 | getColorForIdentifier('postgres')
|
29 | getColorForIdentifier('heroku-postgres')
|
30 |
|
31 | let lineRegex = /^(.*?\[([\w-]+)([\d.]+)?]:)(.*)?$/
|
32 |
|
33 | const red = c.red
|
34 | const dim = i => c.dim(i)
|
35 | const other = dim
|
36 | const path = i => c.blue(i)
|
37 | const method = i => c.bold.magenta(i)
|
38 | const 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 | }
|
46 | const 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 |
|
56 | function 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 |
|
91 | const 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 |
|
101 | function 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 |
|
135 | function 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 |
|
193 | function 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 |
|
233 | function colorizeRedis (body) {
|
234 | if (body.match(/source=\w+ sample#/)) {
|
235 | body = dim(body)
|
236 | }
|
237 | return body
|
238 | }
|
239 |
|
240 | function 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 |
|
255 | module.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 |
|
286 | module.exports.COLORS = COLORS
|