-
Notifications
You must be signed in to change notification settings - Fork 90
Expand file tree
/
Copy pathSupportDiagnostics.tsx
More file actions
146 lines (132 loc) · 4.78 KB
/
Copy pathSupportDiagnostics.tsx
File metadata and controls
146 lines (132 loc) · 4.78 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
import * as React from 'react'
import * as M from '@material-ui/core'
import * as Lab from '@material-ui/lab'
import * as Sentry from '@sentry/react'
import * as APIConnector from 'utils/APIConnector'
// The registry names the archive after the collection run, and support asks for
// that name when several bundles are in flight. Readable cross-origin only
// because the endpoint's CORS config exposes Content-Disposition.
const FILENAME_RE = /filename="([^"]+)"/
const FALLBACK_FILENAME = 'quilt-support-diagnostics.zip'
function getFilename(response: Response): string {
const match = FILENAME_RE.exec(response.headers.get('Content-Disposition') || '')
return match?.[1] || FALLBACK_FILENAME
}
function saveAs(blob: Blob, filename: string) {
const url = URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = filename
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
// Not synchronously: clicking only queues the download, and revoking the URL
// before the browser reads the blob cancels it in some of them.
setTimeout(() => URL.revokeObjectURL(url), 60000)
}
interface Failure {
severity: 'error' | 'warning' | 'info'
message: string
}
// Keyed on the registry's error_code, not on the HTTP status: this stack having
// no collector answers 503 and so does a registry that is merely cycling, and
// only the code tells them apart. Anything unrecognised -- including every error
// raised before the endpoint is reached, by the ALB or the nginx sidecar -- keeps
// the red alert, which is the right reading for an infrastructure failure.
const SEVERITIES: Record<string, Failure['severity']> = {
NotAvailable: 'info',
AlreadyRunning: 'warning',
}
// An error from anything other than the endpoint carries whatever body that thing
// serves, usually an HTML page. APIConnector puts the raw text in `message` when
// it will not parse as JSON, so showing it would paste a document into the alert.
const GENERIC_FAILURE =
'Could not collect diagnostics. The registry may be restarting or unreachable; try again in a few minutes.'
function describe(e: APIConnector.HTTPError): Failure {
const code = e.json?.error_code
if (!code) return { severity: 'error', message: GENERIC_FAILURE }
return {
severity: SEVERITIES[code] || 'error',
message: e.json?.message || GENERIC_FAILURE,
}
}
const useStyles = M.makeStyles((t) => ({
actions: {
alignItems: 'center',
display: 'flex',
marginTop: t.spacing(2),
},
progress: {
marginLeft: t.spacing(2),
},
progressText: {
color: t.palette.text.secondary,
marginLeft: t.spacing(1),
},
failure: {
marginTop: t.spacing(2),
},
}))
export default function SupportDiagnostics() {
const classes = useStyles()
const req = APIConnector.use()
const [collecting, setCollecting] = React.useState(false)
const [failure, setFailure] = React.useState<Failure | null>(null)
const collect = React.useCallback(async () => {
setCollecting(true)
setFailure(null)
try {
const response: Response = await req({
endpoint: '/admin/support-diagnostics',
method: 'POST',
// The response is an archive, and there is no request body to encode.
json: false,
})
saveAs(await response.blob(), getFilename(response))
} catch (e) {
if (e instanceof APIConnector.HTTPError) {
setFailure(describe(e))
} else {
Sentry.captureException(e)
setFailure({ severity: 'error', message: GENERIC_FAILURE })
}
} finally {
setCollecting(false)
}
}, [req])
return (
<>
<M.Typography variant="body2">
Collect a diagnostics bundle describing this stack's search cluster and
infrastructure, for Quilt support to debug against. Nothing is sent anywhere: the
bundle downloads to your computer, and the <code>manifest.json</code> inside it
lists exactly what was collected, so you can review it before attaching it to a
support request.
</M.Typography>
<div className={classes.actions}>
<M.Button
variant="contained"
color="primary"
onClick={collect}
disabled={collecting}
>
Collect diagnostics
</M.Button>
{collecting && (
<>
<M.CircularProgress size={20} className={classes.progress} />
<M.Typography variant="body2" className={classes.progressText}>
Collecting… A large or unhealthy cluster takes longer; closing this
tab loses the download.
</M.Typography>
</>
)}
</div>
{failure && (
<Lab.Alert severity={failure.severity} className={classes.failure}>
{failure.message}
</Lab.Alert>
)}
</>
)
}