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 | 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 |
|
95 | const 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 |
|
105 | function 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 |
|
139 | function 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 |
|
197 | function 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 |
|
237 | function colorizeRedis (body) {
|
238 | if (body.match(/source=\w+ sample#/)) {
|
239 | body = dim(body)
|
240 | }
|
241 | return body
|
242 | }
|
243 |
|
244 | function 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 |
|
259 | module.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 |
|
290 | module.exports.COLORS = COLORS
|