1 | 'use strict'
|
2 |
|
3 | const co = require('co')
|
4 | const cli = require('heroku-cli-util')
|
5 | const {round, flatten, mean, groupBy, map, sum, sumBy, toPairs, sortBy, zip} = require('lodash')
|
6 |
|
7 | let empty = (o) => Object.keys(o).length === 0
|
8 |
|
9 | function 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 |
|
15 | function 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 |
|
28 | function 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 |
|
54 | function displayNotifications (notifications) {
|
55 | if (!notifications) return
|
56 |
|
57 | notifications = notifications.filter((n) => !n.read)
|
58 | if (notifications.length > 0) {
|
59 | cli.log(`
|
60 | You have ${cli.color.yellow(notifications.length)} unread notifications. Read them with ${cli.color.cmd('heroku notifications')}`)
|
61 | }
|
62 | }
|
63 |
|
64 | let dim = (s) => cli.color.dim(s)
|
65 | let bold = (s) => cli.color.bold(s)
|
66 | let label = (s) => cli.color.blue(s)
|
67 |
|
68 | function 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 |
|
89 | function * run (context, heroku) {
|
90 | const img = require('term-img')
|
91 | const path = require('path')
|
92 |
|
93 |
|
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(`
|
153 | See other CLI commands with ${cli.color.cmd('heroku help')}
|
154 | `)
|
155 | }
|
156 |
|
157 | module.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 | }
|