Skip to content

Commit

Permalink
feat(springboot-plugin) Add Info and Health pages
Browse files Browse the repository at this point in the history
  • Loading branch information
mmelko committed Nov 16, 2023
1 parent 1898cb0 commit e019d80
Show file tree
Hide file tree
Showing 7 changed files with 267 additions and 49 deletions.
165 changes: 145 additions & 20 deletions packages/hawtio/src/plugins/springboot/Health.tsx
Original file line number Diff line number Diff line change
@@ -1,27 +1,152 @@
import React from 'react'
import { Card, CardBody, CardHeader, Grid, GridItem, PageSection, Title } from '@patternfly/react-core'
import React, { useEffect, useState } from 'react'
import {
Card,
CardBody,
CardHeader,
Flex,
FlexItem,
Grid,
GridItem,
gridSpans,
PageSection,
Title,
} from '@patternfly/react-core'
import { loadHealth } from '@hawtiosrc/plugins/springboot/springboot-service'
import { HealthComponentDetail, HealthData } from '@hawtiosrc/plugins/springboot/types'
import { TableComposable, Tbody, Td, Tr } from '@patternfly/react-table'
import { humanizeLabels } from '@hawtiosrc/util/strings'
import { ChartDonutUtilization } from '@patternfly/react-charts'
import {
CheckCircleIcon,
ExclamationCircleIcon,
ExclamationTriangleIcon,
InfoCircleIcon,
QuestionCircleIcon,
} from '@patternfly/react-icons'

const componentSpanRecord: Record<string, number[]> = {
diskSpace: [7, 2],
ping: [5, 0],
camelHealth: [5, 1],
}
const ComponentDetails: React.FunctionComponent<{ componentDetails: HealthComponentDetail[] }> = ({
componentDetails,
}) => {
return (
<TableComposable variant='compact' borders={false}>
<Tbody style={{ fontSize: 'xx-small' }}>
{componentDetails.map((detail, index) => {
return (
<Tr key={'row' + detail.key + index}>
<Td key={detail.key + index}>{humanizeLabels(detail.key)}:</Td>
<Td>
{typeof detail.value === 'string' ? detail.value : <ComponentDetails componentDetails={detail.value} />}
</Td>
</Tr>
)
})}
</Tbody>
</TableComposable>
)
}
const HealthStatusIcon: React.FunctionComponent<{ status: string }> = ({ status }) => {
switch (status) {
case 'UP':
return <CheckCircleIcon color={'green'} />
case 'DOWN':
return <ExclamationCircleIcon color={'red'} />
case 'OUT_OF_SERVICE':
return <ExclamationTriangleIcon color={'orange'} />
case 'UNKNOWN':
return <QuestionCircleIcon />
default:
return <InfoCircleIcon color={'blue'} />
}
}

const DiskComponentDetails: React.FunctionComponent<{ componentDetails: HealthComponentDetail[] }> = ({
componentDetails,
}) => {
const total = Number.parseInt(componentDetails.find(k => k.key === 'total')!.value as string)
const free = Number.parseInt(componentDetails.find(k => k.key === 'free')!.value as string)
const usedPercentage = Math.round(((total - free) * 100) / total)

return (
<Grid height={'100%'}>
<GridItem span={6}>
<ComponentDetails componentDetails={componentDetails} />
</GridItem>
<GridItem span={6}>
<ChartDonutUtilization
ariaDesc='Storage capacity'
ariaTitle='Donut utilization chart example'
constrainToVisibleArea
data={{ x: 'Used Space', y: usedPercentage }}
name='chart2'
subTitle='of available space'
title={`${usedPercentage}% used`}
thresholds={[{ value: 90 }]}
width={435}
/>
</GridItem>
</Grid>
)
}
export const Health: React.FunctionComponent = () => {
loadHealth()
const [healthData, setHealthData] = useState<HealthData>()

useEffect(() => {
loadHealth().then(healthData => {
setHealthData(healthData)
})
}, [])

return (
<PageSection variant='light'>
<Grid hasGutter span={12}>
<GridItem>
<Card>
<CardHeader>
<Title headingLevel='h2'>System</Title>
</CardHeader>
<CardBody></CardBody>
</Card>
<Card>
<CardHeader>
<Title headingLevel='h2'>System</Title>
</CardHeader>
<CardBody></CardBody>
</Card>
</GridItem>
</Grid>
<PageSection variant='default'>
{healthData && (
<Grid hasGutter span={4}>
<GridItem span={12}>
<Card>
<CardHeader>
<Flex>
<HealthStatusIcon status={healthData?.status} />
<Title headingLevel='h3'>
<span>Overall status: {healthData?.status}</span>
</Title>
</Flex>
</CardHeader>
</Card>
</GridItem>
{healthData?.components.map(component => (
<GridItem
// span={12}
span={componentSpanRecord[component.name]![0] as gridSpans}
key={component.name}
mdRowSpan={componentSpanRecord[component.name]![1] as gridSpans}
>
<Card>
<CardHeader>
<Title headingLevel='h3'>{humanizeLabels(component.name!)}</Title>
</CardHeader>
<CardBody>
<Flex>
<FlexItem>
<HealthStatusIcon status={component.status} />
</FlexItem>
<FlexItem>Status: {component.status}</FlexItem>
{component.details &&
(component.name === 'diskSpace' ? (
<DiskComponentDetails componentDetails={component.details} />
) : (
<ComponentDetails componentDetails={component.details} />
))}
</Flex>
</CardBody>
</Card>
</GridItem>
))}
</Grid>
)}
</PageSection>
)
}
45 changes: 38 additions & 7 deletions packages/hawtio/src/plugins/springboot/Info.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,39 @@
import React from "react"
import {PageSection} from "@patternfly/react-core"
import React, { useEffect, useState } from 'react'
import { FormGroup, PageSection } from '@patternfly/react-core'
import { getInfo } from '@hawtiosrc/plugins/springboot/springboot-service'
import { TableComposable, Tbody, Td, Th, Thead, Tr } from '@patternfly/react-table'

export const Info:React.FunctionComponent = () => (
<PageSection variant='light'>
Info - TO BE DONE
</PageSection>
)
export const Info: React.FunctionComponent = () => {
const [systemProperties, setSystemProperties] = useState<{ key: string; value: string }[]>([])

useEffect(() => {
getInfo().then(res => {
setSystemProperties(res)
})
}, [])

return (
<PageSection variant='light'>
<FormGroup>
<TableComposable aria-label='Message Table' variant='compact' height='80vh' isStriped isStickyHeader>
<Thead>
<Tr>
<Th data-testid={'name-header'}>Property Name</Th>
<Th data-testid={'value-header'}>Property Value</Th>
</Tr>
</Thead>
<Tbody>
{systemProperties.map((prop, index) => {
return (
<Tr key={'row' + index} data-testid={'row' + index}>
<Td style={{ width: '20%' }}>{prop.key}</Td>
<Td style={{ flex: 3 }}>{prop.value}</Td>
</Tr>
)
})}
</Tbody>
</TableComposable>
</FormGroup>
</PageSection>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,17 @@ import { Nav, NavItem, NavList, PageGroup, PageNavigation, PageSection, Title }
import React from 'react'

import { Navigate, NavLink, Route, Routes, useLocation } from 'react-router-dom'
import {Health} from "@hawtiosrc/plugins/springboot/Health"
import {Info} from "@hawtiosrc/plugins/springboot/Info"
import {Loggers} from "@hawtiosrc/plugins/springboot/Loggers"
import {Trace} from "@hawtiosrc/plugins/springboot/Trace"

import { Health } from '@hawtiosrc/plugins/springboot/Health'
import { Info } from '@hawtiosrc/plugins/springboot/Info'
import { Loggers } from '@hawtiosrc/plugins/springboot/Loggers'
import { Trace } from '@hawtiosrc/plugins/springboot/Trace'

type NavItem = {
id: string
title: string
component: JSX.Element
}
export const Springboot: React.FunctionComponent = () => {
export const SpringBoot: React.FunctionComponent = () => {
const location = useLocation()

const navItems: NavItem[] = [
Expand All @@ -26,7 +25,7 @@ export const Springboot: React.FunctionComponent = () => {
return (
<React.Fragment>
<PageSection variant='light'>
<Title headingLevel='h1'>Springboot</Title>
<Title headingLevel='h1'>Spring Boot</Title>
</PageSection>
<PageGroup>
<PageNavigation>
Expand Down
4 changes: 2 additions & 2 deletions packages/hawtio/src/plugins/springboot/help.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@ Displays the current health status of the application together with details retu

### Info

Displays relevant information regarding the Spring boot application. It is the output of the Info actuator endpoint.
Displays relevant information regarding the Spring boot application. It is the output of the Info actuator endpoint.

### Loggers

Lists all the available loggers in the application. You can modify the level of a logger and the changes will take effect immediately.
Lists all the available loggers in the application. You can modify the level of a logger and the changes will take effect immediately.

### Trace

Expand Down
10 changes: 5 additions & 5 deletions packages/hawtio/src/plugins/springboot/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@ import { pluginId, pluginPath } from './globals'
import { workspace } from '@hawtiosrc/plugins'
import { helpRegistry } from '@hawtiosrc/help'
import help from './help.md'
import {Springboot} from "@hawtiosrc/plugins/springboot/Springboot"
import { SpringBoot } from '@hawtiosrc/plugins/springboot/SpringBoot'

export const springboot: HawtioPlugin = () => {
hawtio.addPlugin({
id: pluginId,
title: 'Springboot',
title: 'Spring Boot',
path: pluginPath,
component: Springboot,
isActive: async () => workspace.hasMBeans(),
component: SpringBoot,
isActive: async () => workspace.treeContainsDomainAndProperties('org.springframework.boot'),
})
helpRegistry.add(pluginId, 'Runtime', help, 16)
helpRegistry.add(pluginId, 'Spring Boot', help, 17)
}
50 changes: 42 additions & 8 deletions packages/hawtio/src/plugins/springboot/springboot-service.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,45 @@
import { SystemProperty } from '@hawtiosrc/plugins/runtime/types'
import { jolokiaService } from '@hawtiosrc/plugins'
import { HealthComponent, HealthData, JolokiaHealthData } from './types'

export async function loadHealth() {
const attr = await jolokiaService.execute('org.springframework.boot:type=Endpoint,name=Health', 'health')
console.log(attr)
// for (const [k, v] of Object.entries(attr as object)) {
// systemProperties.push({ key: k, value: v })
// }
//return systemProperties
export async function loadHealth(): Promise<HealthData> {
const data = (await jolokiaService.execute(
'org.springframework.boot:type=Endpoint,name=Health',
'health',
)) as JolokiaHealthData
let healthComponents: HealthComponent[] = []

healthComponents = Object.entries(data.components).map(([componentName, component]) => {
let details

if (component.details) {
details = Object.entries(component.details).map(([detailKey, detailValue]) => {
const typ = typeof detailValue
const value = ['string', 'number', 'boolean'].includes(typ)
? detailValue.toString()
: Object.entries(detailValue).map(([key, value]) => ({ key, value }))

return {
key: detailKey,
value: value,
}
})
}

return {
name: componentName,
status: component.status,
details: component.details ? details : undefined,
}
})

return { status: data.status, components: healthComponents }
}

export async function getInfo() {
const res = await jolokiaService.execute('org.springframework.boot:type=Endpoint,name=Info', 'info')
const properties: { key: string; value: string }[] = Object.entries(res as object).map(([key, value]) => ({
key,
value,
}))
return properties
}
29 changes: 29 additions & 0 deletions packages/hawtio/src/plugins/springboot/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
export type HealthComponent = {
name: string
status: string
details?: HealthComponentDetail[]
}
export type HealthComponentDetail = {
key: string
value: string | HealthComponentDetail[]
}
export type HealthData = {
components: HealthComponent[]
status: string
}

export type JolokiaHealthData = {
status: string
components: {
[name: string]: {
details?: {
[key: string]:
| string
| {
[key: string]: string
}
}
status: string
}
}
}

0 comments on commit e019d80

Please sign in to comment.