UNPKG

6.3 kBJavaScriptView Raw
1'use strict'
2
3const co = require('co')
4const cli = require('heroku-cli-util')
5const {round, flatten, mean, groupBy, map, sum, sumBy, toPairs, sortBy, zip} = require('lodash')
6
7let empty = (o) => Object.keys(o).length === 0
8
9function displayFormation (formation) {
10 formation = groupBy(formation, 'size')
11 formation = map(formation, (p, size) => `${bold(sumBy(p, 'quantity'))} | ${size}`)
12 cli.log(` ${label('Dynos:')} ${formation.join(', ')}`)
13}
14
15function displayErrors (metrics) {
16 let errors = []
17 if (metrics.routerErrors) {
18 errors = errors.concat(toPairs(metrics.routerErrors.data).map((e) => cli.color.red(`${sum(e[1])} ${e[0]}`)))
19 }
20 if (metrics.dynoErrors) {
21 metrics.dynoErrors.filter((d) => d).forEach((dynoErrors) => {
22 errors = errors.concat(toPairs(dynoErrors.data).map((e) => cli.color.red(`${sum(e[1])} ${e[0]}`)))
23 })
24 }
25 if (errors.length > 0) cli.log(` ${label('Errors:')} ${errors.join(dim(', '))} (see details with ${cli.color.cmd('heroku apps:errors')})`)
26}
27
28function displayMetrics (metrics) {
29 function rpmSparkline () {
30 if (['win32', 'windows'].includes(process.platform)) return ''
31 let sparkline = require('sparkline')
32 let points = []
33 Object.values(metrics.routerStatus.data).forEach((cur) => {
34 for (let i = 0; i < cur.length; i++) {
35 let j = Math.floor(i / 3)
36 points[j] = (points[j] || 0) + cur[i]
37 }
38 })
39 points.pop()
40 return dim(sparkline(points)) + ' last 24 hours rpm'
41 }
42 let ms = ''
43 let rpm = ''
44 if (metrics.routerLatency && !empty(metrics.routerLatency.data)) {
45 let latency = metrics.routerLatency.data['latency.ms.p50']
46 if (!empty(latency)) ms = `${round(mean(latency))} ms `
47 }
48 if (metrics.routerStatus && !empty(metrics.routerStatus.data)) {
49 rpm = `${round(sum(flatten(Object.values(metrics.routerStatus.data))) / 24 / 60)} rpm ${rpmSparkline()}`
50 }
51 if (rpm || ms) cli.log(` ${label('Metrics:')} ${ms}${rpm}`)
52}
53
54function displayNotifications (notifications) {
55 if (!notifications) return
56
57 notifications = notifications.filter((n) => !n.read)
58 if (notifications.length > 0) {
59 cli.log(`
60You have ${cli.color.yellow(notifications.length)} unread notifications. Read them with ${cli.color.cmd('heroku notifications')}`)
61 }
62}
63
64let dim = (s) => cli.color.dim(s)
65let bold = (s) => cli.color.bold(s)
66let label = (s) => cli.color.blue(s)
67
68function displayApps (apps, appsMetrics) {
69 const time = require('../time')
70
71 let owner = (owner) => owner.email.endsWith('@herokumanager.com') ? owner.email.split('@')[0] : owner.email
72
73 for (let a of zip(apps, appsMetrics)) {
74 let app = a[0]
75 let metrics = a[1]
76 cli.log(cli.color.app(app.app.name))
77 cli.log(` ${label('Owner:')} ${owner(app.app.owner)}`)
78 if (app.pipeline) {
79 cli.log(` ${label('Pipeline:')} ${app.pipeline.pipeline.name}`)
80 }
81 displayFormation(app.formation)
82 cli.log(` ${label('Last release:')} ${time.ago(new Date(app.app.released_at))}`)
83 displayMetrics(metrics)
84 displayErrors(metrics)
85 cli.log()
86 }
87}
88
89function * run (context, heroku) {
90 const img = require('term-img')
91 const path = require('path')
92
93 // if not testing and not logged in
94 if (!cli.raiseErrors && (!context.auth || !context.auth.password)) {
95 let {execSync} = require('child_process')
96 execSync('heroku help', {stdio: 'inherit'})
97 return
98 }
99
100 function favoriteApps () {
101 return heroku.request({
102 host: 'longboard.heroku.com',
103 path: '/favorites?type=app',
104 headers: {Range: ''}
105 }).then((apps) => apps.map((app) => app.app_name))
106 }
107
108 function fetchMetrics (apps) {
109 const NOW = new Date().toISOString()
110 const YESTERDAY = new Date(new Date().getTime() - (24 * 60 * 60 * 1000)).toISOString()
111 let date = `start_time=${YESTERDAY}&end_time=${NOW}&step=1h`
112 return apps.map((app) => {
113 let types = app.formation.map((p) => p.type)
114 return {
115 dynoErrors: types.map((type) => heroku.request({host: 'api.metrics.herokai.com', path: `/apps/${app.app.name}/formation/${type}/metrics/errors?${date}`, headers: {Range: ''}}).catch(() => null)),
116 routerLatency: heroku.request({host: 'api.metrics.herokai.com', path: `/apps/${app.app.name}/router-metrics/latency?${date}&process_type=${types[0]}`, headers: {Range: ''}}).catch(() => null),
117 routerErrors: heroku.request({host: 'api.metrics.herokai.com', path: `/apps/${app.app.name}/router-metrics/errors?${date}&process_type=${types[0]}`, headers: {Range: ''}}).catch(() => null),
118 routerStatus: heroku.request({host: 'api.metrics.herokai.com', path: `/apps/${app.app.name}/router-metrics/status?${date}&process_type=${types[0]}`, headers: {Range: ''}}).catch(() => null)
119 }
120 })
121 }
122
123 let apps, data, metrics
124
125 try {
126 img(path.join(__dirname, '..', '..', 'assets', 'heroku.png'), {fallback: () => {}})
127 } catch (err) { }
128
129 yield cli.action('Loading', {clear: true}, co(function * () {
130 apps = yield favoriteApps()
131
132 data = yield {
133 orgs: heroku.request({path: '/organizations'}),
134 notifications: heroku.request({host: 'telex.heroku.com', path: '/user/notifications'}).catch(() => null),
135 apps: apps.map((app) => ({
136 app: heroku.get(`/apps/${app}`),
137 formation: heroku.get(`/apps/${app}/formation`),
138 pipeline: heroku.get(`/apps/${app}/pipeline-couplings`).catch(() => null)
139 }))
140 }
141 metrics = yield fetchMetrics(data.apps)
142 }))
143
144 if (apps.length > 0) displayApps(data.apps, metrics)
145 else cli.warn(`Add apps to this dashboard by favoriting them with ${cli.color.cmd('heroku apps:favorites:add')}`)
146
147 cli.log(`See all add-ons with ${cli.color.cmd('heroku addons')}`)
148 let sampleOrg = sortBy(data.orgs.filter((o) => o.role !== 'collaborator'), (o) => new Date(o.created_at))[0]
149 if (sampleOrg) cli.log(`See all apps in ${cli.color.yellow.dim(sampleOrg.name)} with ${cli.color.cmd('heroku apps --team ' + sampleOrg.name)}`)
150 cli.log(`See all apps with ${cli.color.cmd('heroku apps --all')}`)
151 displayNotifications(data.notifications)
152 cli.log(`
153See other CLI commands with ${cli.color.cmd('heroku help')}
154`)
155}
156
157module.exports = {
158 topic: 'dashboard',
159 description: 'display information about favorite apps',
160 hidden: true,
161 needsAuth: true,
162 run: cli.command(co.wrap(run))
163}