feat(site): add health warning and a health monitor page (#8844)

This commit is contained in:
Bruno Quaresma 2023-08-02 14:49:24 -03:00 committed by GitHub
parent 44f9b0228a
commit cf35c0dfc5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 814 additions and 15 deletions

6
coderd/apidoc/docs.go generated
View File

@ -8024,7 +8024,8 @@ const docTemplate = `{
"tailnet_pg_coordinator",
"single_tailnet",
"template_restart_requirement",
"template_insights_page"
"template_insights_page",
"deployment_health_page"
],
"x-enum-varnames": [
"ExperimentMoons",
@ -8032,7 +8033,8 @@ const docTemplate = `{
"ExperimentTailnetPGCoordinator",
"ExperimentSingleTailnet",
"ExperimentTemplateRestartRequirement",
"ExperimentTemplateInsightsPage"
"ExperimentTemplateInsightsPage",
"ExperimentDeploymentHealthPage"
]
},
"codersdk.Feature": {

View File

@ -7185,7 +7185,8 @@
"tailnet_pg_coordinator",
"single_tailnet",
"template_restart_requirement",
"template_insights_page"
"template_insights_page",
"deployment_health_page"
],
"x-enum-varnames": [
"ExperimentMoons",
@ -7193,7 +7194,8 @@
"ExperimentTailnetPGCoordinator",
"ExperimentSingleTailnet",
"ExperimentTemplateRestartRequirement",
"ExperimentTemplateInsightsPage"
"ExperimentTemplateInsightsPage",
"ExperimentDeploymentHealthPage"
]
},
"codersdk.Feature": {

View File

@ -1871,6 +1871,9 @@ const (
// Insights page
ExperimentTemplateInsightsPage Experiment = "template_insights_page"
// Deployment health page
ExperimentDeploymentHealthPage Experiment = "deployment_health_page"
// Add new experiments here!
// ExperimentExample Experiment = "example"
)
@ -1881,6 +1884,7 @@ const (
// not be included here and will be essentially hidden.
var ExperimentsAll = Experiments{
ExperimentTemplateInsightsPage,
ExperimentDeploymentHealthPage,
}
// Experiments is a list of experiments that are enabled for the deployment.

1
docs/api/schemas.md generated
View File

@ -2677,6 +2677,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
| `single_tailnet` |
| `template_restart_requirement` |
| `template_insights_page` |
| `deployment_health_page` |
## codersdk.Feature

View File

@ -183,6 +183,7 @@ const TemplateInsightsPage = lazy(
() =>
import("./pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage"),
)
const HealthPage = lazy(() => import("./pages/HealthPage/HealthPage"))
export const AppRouter: FC = () => {
return (
@ -197,6 +198,8 @@ export const AppRouter: FC = () => {
<Route element={<DashboardLayout />}>
<Route index element={<IndexPage />} />
<Route path="health" element={<HealthPage />} />
<Route path="gitauth/:provider" element={<GitAuthPage />} />
<Route path="workspaces" element={<WorkspacesPage />} />

View File

@ -1405,3 +1405,15 @@ export const getInsightsTemplate = async (
const response = await axios.get(`/api/v2/insights/templates?${params}`)
return response.data
}
export const getHealth = () => {
return axios.get<{
healthy: boolean
time: string
coder_version: string
derp: { healthy: boolean }
access_url: { healthy: boolean }
websocket: { healthy: boolean }
database: { healthy: boolean }
}>("/api/v2/debug/health")
}

View File

@ -1563,6 +1563,7 @@ export const Entitlements: Entitlement[] = [
// From codersdk/deployment.go
export type Experiment =
| "deployment_health_page"
| "moons"
| "single_tailnet"
| "tailnet_pg_coordinator"
@ -1570,6 +1571,7 @@ export type Experiment =
| "template_restart_requirement"
| "workspace_actions"
export const Experiments: Experiment[] = [
"deployment_health_page",
"moons",
"single_tailnet",
"tailnet_pg_coordinator",

View File

@ -16,6 +16,7 @@ import Box from "@mui/material/Box"
import InfoOutlined from "@mui/icons-material/InfoOutlined"
import Button from "@mui/material/Button"
import { docs } from "utils/docs"
import { HealthBanner } from "./HealthBanner"
export const DashboardLayout: FC = () => {
const styles = useStyles()
@ -30,6 +31,7 @@ export const DashboardLayout: FC = () => {
return (
<>
<HealthBanner />
<ServiceBanner />
{canViewDeployment && <LicenseBanner />}

View File

@ -0,0 +1,45 @@
import { Alert } from "components/Alert/Alert"
import { Link as RouterLink } from "react-router-dom"
import Link from "@mui/material/Link"
import { colors } from "theme/colors"
import { useQuery } from "@tanstack/react-query"
import { getHealth } from "api/api"
import { useDashboard } from "./DashboardProvider"
export const HealthBanner = () => {
const { data: healthStatus } = useQuery({
queryKey: ["health"],
queryFn: () => getHealth(),
})
const dashboard = useDashboard()
const hasHealthIssues = healthStatus && !healthStatus.data.healthy
if (
dashboard.experiments.includes("deployment_health_page") &&
hasHealthIssues
) {
return (
<Alert
severity="error"
variant="filled"
sx={{
border: 0,
borderRadius: 0,
backgroundColor: colors.red[10],
}}
>
We have detected problems with your Coder deployment. Please{" "}
<Link
component={RouterLink}
to="/health"
sx={{ fontWeight: 600, color: "inherit" }}
>
inspect the health status
</Link>
.
</Alert>
)
}
return null
}

View File

@ -6,6 +6,7 @@ import LockRounded from "@mui/icons-material/LockOutlined"
import Globe from "@mui/icons-material/PublicOutlined"
import HubOutlinedIcon from "@mui/icons-material/HubOutlined"
import VpnKeyOutlined from "@mui/icons-material/VpnKeyOutlined"
import MonitorHeartOutlined from "@mui/icons-material/MonitorHeartOutlined"
import { GitIcon } from "components/Icons/GitIcon"
import { Stack } from "components/Stack/Stack"
import { ElementType, PropsWithChildren, ReactNode, FC } from "react"
@ -93,6 +94,14 @@ export const Sidebar: React.FC = () => {
>
Security
</SidebarNavItem>
{dashboard.experiments.includes("deployment_health_page") && (
<SidebarNavItem
href="/health"
icon={<SidebarNavItemIcon icon={MonitorHeartOutlined} />}
>
Health
</SidebarNavItem>
)}
</nav>
)
}

View File

@ -1,11 +1,17 @@
import { makeStyles } from "@mui/styles"
import { FC, PropsWithChildren } from "react"
import { combineClasses } from "utils/combineClasses"
export const FullWidthPageHeader: FC<PropsWithChildren> = ({ children }) => {
export const FullWidthPageHeader: FC<
PropsWithChildren & { sticky?: boolean }
> = ({ children, sticky = true }) => {
const styles = useStyles()
return (
<header className={styles.header} data-testid="header">
<header
className={combineClasses([styles.header, sticky ? styles.sticky : ""])}
data-testid="header"
>
{children}
</header>
)
@ -35,8 +41,7 @@ const useStyles = makeStyles((theme) => ({
display: "flex",
alignItems: "center",
gap: theme.spacing(6),
position: "sticky",
top: 0,
zIndex: 10,
flexWrap: "wrap",
@ -48,6 +53,10 @@ const useStyles = makeStyles((theme) => ({
flexDirection: "column",
},
},
sticky: {
position: "sticky",
top: 0,
},
actions: {
marginLeft: "auto",
[theme.breakpoints.down("md")]: {

View File

@ -1,11 +1,12 @@
import Box from "@mui/material/Box"
import { makeStyles } from "@mui/styles"
import { ComponentProps, FC, PropsWithChildren } from "react"
import { combineClasses } from "utils/combineClasses"
export const Stats: FC<ComponentProps<"div">> = (props) => {
export const Stats: FC<ComponentProps<typeof Box>> = (props) => {
const styles = useStyles()
return (
<div
<Box
{...props}
className={combineClasses([styles.stats, props.className])}
/>
@ -16,18 +17,18 @@ export const StatsItem: FC<
{
label: string
value: string | number | JSX.Element
} & ComponentProps<"div">
} & ComponentProps<typeof Box>
> = ({ label, value, ...divProps }) => {
const styles = useStyles()
return (
<div
<Box
{...divProps}
className={combineClasses([styles.statItem, divProps.className])}
>
<span className={styles.statsLabel}>{label}:</span>
<span className={styles.statsValue}>{value}</span>
</div>
</Box>
)
}

View File

@ -1,4 +1,4 @@
import { FC } from "react"
import { ComponentProps, FC } from "react"
import Editor, { DiffEditor, loader } from "@monaco-editor/react"
import * as monaco from "monaco-editor"
import { useCoderTheme } from "./coderTheme"
@ -9,8 +9,10 @@ loader.config({ monaco })
export const SyntaxHighlighter: FC<{
value: string
language: string
editorProps?: ComponentProps<typeof Editor> &
ComponentProps<typeof DiffEditor>
compareWith?: string
}> = ({ value, compareWith, language }) => {
}> = ({ value, compareWith, language, editorProps }) => {
const styles = useStyles()
const hasDiff = compareWith && value !== compareWith
const coderTheme = useCoderTheme()
@ -25,6 +27,7 @@ export const SyntaxHighlighter: FC<{
renderSideBySide: true,
readOnly: true,
},
...editorProps,
}
if (coderTheme.isLoading) {
@ -46,5 +49,6 @@ const useStyles = makeStyles((theme) => ({
wrapper: {
padding: theme.spacing(1, 0),
background: theme.palette.background.paper,
height: "100%",
},
}))

View File

@ -0,0 +1,33 @@
import { Meta, StoryObj } from "@storybook/react"
import { HealthPageView } from "./HealthPage"
import { MockHealth } from "testHelpers/entities"
const meta: Meta<typeof HealthPageView> = {
title: "pages/HealthPageView",
component: HealthPageView,
args: {
tab: {
value: "derp",
set: () => {},
},
healthStatus: MockHealth,
},
}
export default meta
type Story = StoryObj<typeof HealthPageView>
export const HealthPage: Story = {}
export const UnhealthPage: Story = {
args: {
healthStatus: {
...MockHealth,
healthy: false,
derp: {
...MockHealth.derp,
healthy: false,
},
},
},
}

View File

@ -0,0 +1,247 @@
import Box from "@mui/material/Box"
import { useQuery } from "@tanstack/react-query"
import { getHealth } from "api/api"
import { Loader } from "components/Loader/Loader"
import { useTab } from "hooks"
import { Helmet } from "react-helmet-async"
import { pageTitle } from "utils/page"
import { colors } from "theme/colors"
import CheckCircleOutlined from "@mui/icons-material/CheckCircleOutlined"
import ErrorOutline from "@mui/icons-material/ErrorOutline"
import { SyntaxHighlighter } from "components/SyntaxHighlighter/SyntaxHighlighter"
import { Stack } from "components/Stack/Stack"
import {
FullWidthPageHeader,
PageHeaderTitle,
PageHeaderSubtitle,
} from "components/PageHeader/FullWidthPageHeader"
import { Stats, StatsItem } from "components/Stats/Stats"
import { makeStyles } from "@mui/styles"
import { createDayString } from "utils/createDayString"
const sections = {
derp: "DERP",
access_url: "Access URL",
websocket: "Websocket",
database: "Database",
} as const
export default function HealthPage() {
const tab = useTab("tab", "derp")
const { data: healthStatus } = useQuery({
queryKey: ["health"],
queryFn: () => getHealth(),
refetchInterval: 10_000,
})
return (
<>
<Helmet>
<title>{pageTitle("Health")}</title>
</Helmet>
{healthStatus ? (
<HealthPageView healthStatus={healthStatus.data} tab={tab} />
) : (
<Loader />
)}
</>
)
}
export function HealthPageView({
healthStatus,
tab,
}: {
healthStatus: Awaited<ReturnType<typeof getHealth>>["data"]
tab: ReturnType<typeof useTab>
}) {
const styles = useStyles()
return (
<Box
sx={{
height: "calc(100vh - 62px - 36px)",
overflow: "hidden",
// Remove padding added from dashboard layout (.siteContent)
marginBottom: "-48px",
}}
>
<FullWidthPageHeader sticky={false}>
<Stack direction="row" spacing={2} alignItems="center">
{healthStatus.healthy ? (
<CheckCircleOutlined
sx={{
width: 32,
height: 32,
color: (theme) => theme.palette.success.light,
}}
/>
) : (
<ErrorOutline
sx={{
width: 32,
height: 32,
color: (theme) => theme.palette.error.main,
}}
/>
)}
<div>
<PageHeaderTitle>
{healthStatus.healthy ? "Healthy" : "Unhealthy"}
</PageHeaderTitle>
<PageHeaderSubtitle>
{healthStatus.healthy
? "All systems operational"
: "Some issues have been detected"}
</PageHeaderSubtitle>
</div>
</Stack>
<Stats aria-label="Deployment details" className={styles.stats}>
<StatsItem
className={styles.statsItem}
label="Last check"
value={createDayString(healthStatus.time)}
/>
<StatsItem
className={styles.statsItem}
label="Coder version"
value={healthStatus.coder_version}
/>
</Stats>
</FullWidthPageHeader>
<Box
sx={{
display: "flex",
alignItems: "start",
height: "100%",
}}
>
<Box
sx={{
width: (theme) => theme.spacing(32),
flexShrink: 0,
borderRight: (theme) => `1px solid ${theme.palette.divider}`,
height: "100%",
}}
>
<Box
sx={{
fontSize: 10,
textTransform: "uppercase",
fontWeight: 500,
color: (theme) => theme.palette.text.secondary,
padding: (theme) => theme.spacing(1.5, 3),
letterSpacing: "0.5px",
}}
>
Health
</Box>
<Box component="nav">
{Object.keys(sections)
.sort()
.map((key) => {
const label = sections[key as keyof typeof sections]
const isActive = tab.value === key
const isHealthy =
healthStatus[key as keyof typeof sections].healthy
return (
<Box
component="button"
key={key}
onClick={() => {
tab.set(key)
}}
sx={{
background: isActive ? colors.gray[13] : "none",
border: "none",
fontSize: 14,
width: "100%",
display: "flex",
alignItems: "center",
gap: 1,
textAlign: "left",
height: 36,
padding: (theme) => theme.spacing(0, 3),
cursor: "pointer",
pointerEvents: isActive ? "none" : "auto",
color: (theme) =>
isActive
? theme.palette.text.primary
: theme.palette.text.secondary,
"&:hover": {
background: (theme) => theme.palette.action.hover,
color: (theme) => theme.palette.text.primary,
},
}}
>
{isHealthy ? (
<CheckCircleOutlined
sx={{
width: 16,
height: 16,
color: (theme) => theme.palette.success.light,
}}
/>
) : (
<ErrorOutline
sx={{
width: 16,
height: 16,
color: (theme) => theme.palette.error.main,
}}
/>
)}
{label}
</Box>
)
})}
</Box>
</Box>
{/* 62px - navbar and 36px - the bottom bar */}
<Box sx={{ height: "100%", overflowY: "auto", width: "100%" }}>
<SyntaxHighlighter
language="json"
editorProps={{ height: "100%" }}
value={JSON.stringify(
healthStatus[tab.value as keyof typeof healthStatus],
null,
2,
)}
/>
</Box>
</Box>
</Box>
)
}
const useStyles = makeStyles((theme) => ({
stats: {
padding: 0,
border: 0,
gap: theme.spacing(6),
rowGap: theme.spacing(3),
flex: 1,
[theme.breakpoints.down("md")]: {
display: "flex",
flexDirection: "column",
alignItems: "flex-start",
gap: theme.spacing(1),
},
},
statsItem: {
flexDirection: "column",
gap: 0,
padding: 0,
"& > span:first-of-type": {
fontSize: 12,
fontWeight: 500,
},
},
}))

View File

@ -1862,3 +1862,422 @@ export const MockLicenseResponse: GetLicensesResponse[] = [
},
},
]
export const MockHealth = {
time: "2023-08-01T16:51:03.29792825Z",
healthy: true,
failing_sections: null,
derp: {
healthy: true,
regions: {
"999": {
healthy: true,
region: {
EmbeddedRelay: true,
RegionID: 999,
RegionCode: "coder",
RegionName: "Council Bluffs, Iowa",
Nodes: [
{
Name: "999stun0",
RegionID: 999,
HostName: "stun.l.google.com",
STUNPort: 19302,
STUNOnly: true,
},
{
Name: "999b",
RegionID: 999,
HostName: "dev.coder.com",
STUNPort: -1,
DERPPort: 443,
},
],
},
node_reports: [
{
healthy: true,
node: {
Name: "999stun0",
RegionID: 999,
HostName: "stun.l.google.com",
STUNPort: 19302,
STUNOnly: true,
},
node_info: {
TokenBucketBytesPerSecond: 0,
TokenBucketBytesBurst: 0,
},
can_exchange_messages: false,
round_trip_ping: 0,
uses_websocket: false,
client_logs: [],
client_errs: [],
error: null,
stun: {
Enabled: true,
CanSTUN: true,
Error: null,
},
},
{
healthy: true,
node: {
Name: "999b",
RegionID: 999,
HostName: "dev.coder.com",
STUNPort: -1,
DERPPort: 443,
},
node_info: {
TokenBucketBytesPerSecond: 0,
TokenBucketBytesBurst: 0,
},
can_exchange_messages: true,
round_trip_ping: 7674330,
uses_websocket: false,
client_logs: [
[
"derphttp.Client.Connect: connecting to https://dev.coder.com/derp",
],
[
"derphttp.Client.Connect: connecting to https://dev.coder.com/derp",
],
],
client_errs: [[], []],
error: null,
stun: {
Enabled: false,
CanSTUN: false,
Error: null,
},
},
],
error: null,
},
"10007": {
healthy: true,
region: {
EmbeddedRelay: false,
RegionID: 10007,
RegionCode: "coder_sydney",
RegionName: "sydney",
Nodes: [
{
Name: "10007stun0",
RegionID: 10007,
HostName: "stun.l.google.com",
STUNPort: 19302,
STUNOnly: true,
},
{
Name: "10007a",
RegionID: 10007,
HostName: "sydney.dev.coder.com",
STUNPort: -1,
DERPPort: 443,
},
],
},
node_reports: [
{
healthy: true,
node: {
Name: "10007stun0",
RegionID: 10007,
HostName: "stun.l.google.com",
STUNPort: 19302,
STUNOnly: true,
},
node_info: {
TokenBucketBytesPerSecond: 0,
TokenBucketBytesBurst: 0,
},
can_exchange_messages: false,
round_trip_ping: 0,
uses_websocket: false,
client_logs: [],
client_errs: [],
error: null,
stun: {
Enabled: true,
CanSTUN: true,
Error: null,
},
},
{
healthy: true,
node: {
Name: "10007a",
RegionID: 10007,
HostName: "sydney.dev.coder.com",
STUNPort: -1,
DERPPort: 443,
},
node_info: {
TokenBucketBytesPerSecond: 0,
TokenBucketBytesBurst: 0,
},
can_exchange_messages: true,
round_trip_ping: 170527034,
uses_websocket: false,
client_logs: [
[
"derphttp.Client.Connect: connecting to https://sydney.dev.coder.com/derp",
],
[
"derphttp.Client.Connect: connecting to https://sydney.dev.coder.com/derp",
],
],
client_errs: [[], []],
error: null,
stun: {
Enabled: false,
CanSTUN: false,
Error: null,
},
},
],
error: null,
},
"10008": {
healthy: true,
region: {
EmbeddedRelay: false,
RegionID: 10008,
RegionCode: "coder_europe-frankfurt",
RegionName: "europe-frankfurt",
Nodes: [
{
Name: "10008stun0",
RegionID: 10008,
HostName: "stun.l.google.com",
STUNPort: 19302,
STUNOnly: true,
},
{
Name: "10008a",
RegionID: 10008,
HostName: "europe.dev.coder.com",
STUNPort: -1,
DERPPort: 443,
},
],
},
node_reports: [
{
healthy: true,
node: {
Name: "10008stun0",
RegionID: 10008,
HostName: "stun.l.google.com",
STUNPort: 19302,
STUNOnly: true,
},
node_info: {
TokenBucketBytesPerSecond: 0,
TokenBucketBytesBurst: 0,
},
can_exchange_messages: false,
round_trip_ping: 0,
uses_websocket: false,
client_logs: [],
client_errs: [],
error: null,
stun: {
Enabled: true,
CanSTUN: true,
Error: null,
},
},
{
healthy: true,
node: {
Name: "10008a",
RegionID: 10008,
HostName: "europe.dev.coder.com",
STUNPort: -1,
DERPPort: 443,
},
node_info: {
TokenBucketBytesPerSecond: 0,
TokenBucketBytesBurst: 0,
},
can_exchange_messages: true,
round_trip_ping: 111329690,
uses_websocket: false,
client_logs: [
[
"derphttp.Client.Connect: connecting to https://europe.dev.coder.com/derp",
],
[
"derphttp.Client.Connect: connecting to https://europe.dev.coder.com/derp",
],
],
client_errs: [[], []],
error: null,
stun: {
Enabled: false,
CanSTUN: false,
Error: null,
},
},
],
error: null,
},
"10009": {
healthy: true,
region: {
EmbeddedRelay: false,
RegionID: 10009,
RegionCode: "coder_brazil-saopaulo",
RegionName: "brazil-saopaulo",
Nodes: [
{
Name: "10009stun0",
RegionID: 10009,
HostName: "stun.l.google.com",
STUNPort: 19302,
STUNOnly: true,
},
{
Name: "10009a",
RegionID: 10009,
HostName: "brazil.dev.coder.com",
STUNPort: -1,
DERPPort: 443,
},
],
},
node_reports: [
{
healthy: true,
node: {
Name: "10009stun0",
RegionID: 10009,
HostName: "stun.l.google.com",
STUNPort: 19302,
STUNOnly: true,
},
node_info: {
TokenBucketBytesPerSecond: 0,
TokenBucketBytesBurst: 0,
},
can_exchange_messages: false,
round_trip_ping: 0,
uses_websocket: false,
client_logs: [],
client_errs: [],
error: null,
stun: {
Enabled: true,
CanSTUN: true,
Error: null,
},
},
{
healthy: true,
node: {
Name: "10009a",
RegionID: 10009,
HostName: "brazil.dev.coder.com",
STUNPort: -1,
DERPPort: 443,
},
node_info: {
TokenBucketBytesPerSecond: 0,
TokenBucketBytesBurst: 0,
},
can_exchange_messages: true,
round_trip_ping: 138185506,
uses_websocket: false,
client_logs: [
[
"derphttp.Client.Connect: connecting to https://brazil.dev.coder.com/derp",
],
[
"derphttp.Client.Connect: connecting to https://brazil.dev.coder.com/derp",
],
],
client_errs: [[], []],
error: null,
stun: {
Enabled: false,
CanSTUN: false,
Error: null,
},
},
],
error: null,
},
},
netcheck: {
UDP: true,
IPv6: false,
IPv4: true,
IPv6CanSend: false,
IPv4CanSend: true,
OSHasIPv6: true,
ICMPv4: false,
MappingVariesByDestIP: false,
HairPinning: null,
UPnP: false,
PMP: false,
PCP: false,
PreferredDERP: 999,
RegionLatency: {
"999": 1638180,
"10007": 174853022,
"10008": 112142029,
"10009": 138855606,
},
RegionV4Latency: {
"999": 1638180,
"10007": 174853022,
"10008": 112142029,
"10009": 138855606,
},
RegionV6Latency: {},
GlobalV4: "34.71.26.24:55368",
GlobalV6: "",
CaptivePortal: null,
},
netcheck_err: null,
netcheck_logs: [
"netcheck: netcheck.runProbe: got STUN response for 10007stun0 from 34.71.26.24:55368 (9b07930007da49dd7df79bc7) in 1.791799ms",
"netcheck: netcheck.runProbe: got STUN response for 999stun0 from 34.71.26.24:55368 (7397fec097f1d5b01364566b) in 1.791529ms",
"netcheck: netcheck.runProbe: got STUN response for 10008stun0 from 34.71.26.24:55368 (1fdaaa016ca386485f097f68) in 2.192899ms",
"netcheck: netcheck.runProbe: got STUN response for 10009stun0 from 34.71.26.24:55368 (2596fe60895fbd9542823a76) in 2.146459ms",
"netcheck: netcheck.runProbe: got STUN response for 10007stun0 from 34.71.26.24:55368 (19ec320f3b76e8b027b06d3e) in 2.139619ms",
"netcheck: netcheck.runProbe: got STUN response for 999stun0 from 34.71.26.24:55368 (a17973bc57c35e606c0f46f5) in 2.131089ms",
"netcheck: netcheck.runProbe: got STUN response for 10008stun0 from 34.71.26.24:55368 (c958e15209d139a6e410f13a) in 2.127549ms",
"netcheck: netcheck.runProbe: got STUN response for 10009stun0 from 34.71.26.24:55368 (284a1b64dff22f40a3514524) in 2.107549ms",
"netcheck: [v1] measureAllICMPLatency: listen ip4:icmp 0.0.0.0: socket: operation not permitted",
"netcheck: [v1] report: udp=true v6=false v6os=true mapvarydest=false hair= portmap= v4a=34.71.26.24:55368 derp=999 derpdist=999v4:2ms,10007v4:175ms,10008v4:112ms,10009v4:139ms",
],
error: null,
},
access_url: {
access_url: "https://dev.coder.com",
healthy: true,
reachable: true,
status_code: 200,
healthz_response: "OK",
error: null,
},
websocket: {
healthy: true,
response: {
body: "",
code: 101,
},
error: null,
},
database: {
healthy: true,
reachable: true,
latency: 92570,
error: null,
},
coder_version: "v0.27.1-devel+c575292",
}

View File

@ -387,4 +387,8 @@ export const handlers = [
rest.get("/api/v2/workspaceagents/:agent/logs", (_, res, ctx) => {
return res(ctx.status(200), ctx.json(M.MockWorkspaceAgentLogs))
}),
rest.get("/api/v2/debug/health", (_, res, ctx) => {
return res(ctx.status(200), ctx.json(M.MockHealth))
}),
]