Skip to content

Commit

Permalink
feat(connect): add Discover tab - by Jolokia Discovery MBean
Browse files Browse the repository at this point in the history
  • Loading branch information
tadayosi committed Nov 12, 2023
1 parent 8521d8d commit 3dbb373
Show file tree
Hide file tree
Showing 11 changed files with 493 additions and 26 deletions.
203 changes: 201 additions & 2 deletions packages/hawtio/src/plugins/connect/discover/Discover.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,204 @@
import React from 'react'
import { HawtioEmptyCard, HawtioLoadingCard, connectService } from '@hawtiosrc/plugins/shared'
import { formatTimestamp } from '@hawtiosrc/util/dates'
import {
Button,
Card,
CardActions,
CardBody,
CardHeader,
CardTitle,
DescriptionList,
DescriptionListDescription,
DescriptionListGroup,
DescriptionListTerm,
Gallery,
SearchInput,
Text,
Toolbar,
ToolbarContent,
ToolbarItem,
} from '@patternfly/react-core'
import React, { useContext, useEffect, useState } from 'react'
import { ADD, UPDATE } from '../connections'
import { ConnectContext } from '../context'
import { log } from '../globals'
import javaLogo from '../img/java-logo.svg'
import jettyLogo from '../img/jetty-logo.svg'
import tomcatLogo from '../img/tomcat-logo.svg'
import { Agent, discoverService } from './discover-service'

export const Discover: React.FunctionComponent = () => {
return null
const [discoverable, setDiscoverable] = useState(false)
const [discovering, setDiscovering] = useState(true)
const [agents, setAgents] = useState<Agent[]>([])

// Filter
const [filter, setFilter] = useState('')
const [filteredAgents, setFilteredAgents] = useState<Agent[]>([])

useEffect(() => {
if (!discovering) {
return
}

const isDiscoverable = async () => {
const discoverable = await discoverService.isDiscoverable()
setDiscoverable(discoverable)
setDiscovering(false)
}
isDiscoverable()

setDiscovering(true)
const discoverAgents = async () => {
const agents = await discoverService.discoverAgents()
log.debug('Discover - agents:', agents)
setAgents(agents)
setFilteredAgents(agents)
setDiscovering(false)
}
discoverAgents()
}, [discovering])

if (discovering) {
return <HawtioLoadingCard message='Please wait, discovering agents...' />
}

if (!discoverable) {
return <HawtioEmptyCard message='Agent discovery is not available' />
}

const applyFilter = () => {
const filtered = agents.filter(agent =>
Object.values(agent).some(value => typeof value === 'string' && value.includes(filter)),
)
log.debug('Discover - apply filter:', filter, filtered)
setFilteredAgents(filtered)
}

const clearFilter = () => {
setFilter('')
setFilteredAgents(agents)
}

const refresh = () => {
setDiscovering(true)
}

const toolbar = (
<Toolbar id='connect-discover-toolbar'>
<ToolbarContent>
<ToolbarItem id='connect-discover-toolbar-filter'>
<SearchInput
id='connect-discover-toolbar-filter-input'
aria-label='Filter Agents'
placeholder='Filter agents...'
value={filter}
onChange={(_, value) => setFilter(value)}
onSearch={applyFilter}
onClear={clearFilter}
/>
</ToolbarItem>
<ToolbarItem variant='separator' />
<ToolbarItem>
<Button variant='secondary' onClick={refresh} isSmall>
Refresh
</Button>
</ToolbarItem>
</ToolbarContent>
</Toolbar>
)

return (
<React.Fragment>
<Card style={{ marginBottom: '1rem' }}>{toolbar}</Card>
<Gallery hasGutter minWidths={{ default: '400px' }}>
{filteredAgents.map((agent, index) => (
<AgentCard key={`agent-${index}-${agent.agent_id}`} agent={agent} />
))}
</Gallery>
</React.Fragment>
)
}

const PRODUCT_LOGO: Record<string, string> = {
jetty: jettyLogo,
tomcat: tomcatLogo,
generic: javaLogo,
}

export const AgentCard: React.FunctionComponent<{ agent: Agent }> = ({ agent }) => {
const { connections, dispatch } = useContext(ConnectContext)

const connect = () => {
const conn = discoverService.toConnection(agent)
log.debug('Discover - connect to:', conn)

// Save the connection before connecting
if (connections[conn.name]) {
dispatch({ type: UPDATE, name: conn.name, connection: conn })
} else {
dispatch({ type: ADD, connection: conn })
}

connectService.connect(conn)
}

const productLogo = (agent: Agent) => {
return PRODUCT_LOGO[agent.server_product?.toLowerCase() ?? 'generic'] ?? PRODUCT_LOGO.generic
}

return (
<Card isCompact id={`connect-discover-agent-card-${agent.agent_id}`}>
<CardHeader>
<img src={productLogo(agent)} alt={agent.server_product} style={{ maxWidth: '30px', paddingRight: '0.5rem' }} />
{discoverService.hasName(agent) && (
<CardTitle>
{agent.server_vendor} {agent.server_product} {agent.server_version}
</CardTitle>
)}
{agent.command && (
<CardTitle>
<Text component='pre'>{agent.command}</Text>
</CardTitle>
)}
<CardActions>
<Button variant='primary' onClick={connect} isSmall>
Connect
</Button>
</CardActions>
</CardHeader>
<CardBody>
<DescriptionList isCompact isHorizontal>
<DescriptionListGroup>
<DescriptionListTerm>Agent ID</DescriptionListTerm>
<DescriptionListDescription>{agent.agent_id}</DescriptionListDescription>
</DescriptionListGroup>
<DescriptionListGroup>
<DescriptionListTerm>Agent Version</DescriptionListTerm>
<DescriptionListDescription>{agent.agent_version}</DescriptionListDescription>
</DescriptionListGroup>
<DescriptionListGroup>
<DescriptionListTerm>Agent Description</DescriptionListTerm>
<DescriptionListDescription>{agent.agent_description}</DescriptionListDescription>
</DescriptionListGroup>
{agent.startTime && (
<DescriptionListGroup>
<DescriptionListTerm>JVM Started</DescriptionListTerm>
<DescriptionListDescription>{formatTimestamp(new Date(agent.startTime))}</DescriptionListDescription>
</DescriptionListGroup>
)}
{agent.url && (
<DescriptionListGroup>
<DescriptionListTerm>Agent URL</DescriptionListTerm>
<DescriptionListDescription>
<Text component='a' href={agent.url} target='_blank'>
{agent.url}
</Text>
</DescriptionListDescription>
</DescriptionListGroup>
)}
</DescriptionList>
</CardBody>
</Card>
)
}
90 changes: 90 additions & 0 deletions packages/hawtio/src/plugins/connect/discover/discover-service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { Connection, INITIAL_CONNECTION, connectService, jolokiaService, workspace } from '@hawtiosrc/plugins/shared'
import { isBlank } from '@hawtiosrc/util/strings'
import { log } from '../globals'

/**
* @see https://jolokia.org/reference/html/mbeans.html#mbean-discovery
*/
export type Agent = {
// Properties from Jolokia API
agent_id?: string
agent_description?: string
agent_version?: string
url?: string
secured?: boolean
server_vendor?: string
server_product?: string
server_version?: string

// Properties that Hawtio attaches
startTime?: number
command?: string
}

class DiscoverService {
async isDiscoverable(): Promise<boolean> {
return (await this.hasLocalMBean()) || (await this.hasDiscoveryMBean())
}

private hasLocalMBean(): Promise<boolean> {
return workspace.treeContainsDomainAndProperties('hawtio', { type: 'JVMList' })
}

private hasDiscoveryMBean(): Promise<boolean> {
return workspace.treeContainsDomainAndProperties('jolokia', { type: 'Discovery' })
}

async discoverAgents(): Promise<Agent[]> {
// Jolokia 1.x: 'jolokia:type=Discovery'
// Jolokia 2.x: 'jolokia:type=Discovery,agent=...'
const discoveryMBean = (await workspace.findMBeans('jolokia', { type: 'Discovery' }))[0]
if (discoveryMBean && discoveryMBean.objectName) {
// Use 10 sec timeout
const agents = (await jolokiaService.execute(discoveryMBean.objectName, 'lookupAgentsWithTimeout(int)', [
10 * 1000,
])) as Agent[]
await this.fetchMoreJvmDetails(agents)
return agents
}

return []
}

private async fetchMoreJvmDetails(agents: Agent[]) {
for (const agent of agents) {
if (!agent.url || agent.secured) {
continue
}
// One-off Jolokia instance to connect to the agent
const jolokia = connectService.createJolokia(this.toConnection(agent))
agent.startTime = jolokia.getAttribute('java.lang:type=Runtime', 'StartTime') as number
if (!this.hasName(agent)) {
// Only look for command if agent vm is not known
agent.command = jolokia.getAttribute('java.lang:type=Runtime', 'SystemProperties', 'sun.java.command') as string
}
}
}

toConnection(agent: Agent): Connection {
const conn = { ...INITIAL_CONNECTION, name: agent.agent_description ?? `discover-${agent.agent_id}` }
if (!agent.url) {
log.warn('No URL available to connect to agent:', agent)
return conn
}

const url = new URL(agent.url)
conn.scheme = url.protocol.substring(0, url.protocol.length - 1) // strip last ':'
conn.host = url.hostname
conn.port = parseInt(url.port)
conn.path = url.pathname

log.debug('Discover - connection from agent:', conn)
return conn
}

hasName(agent: Agent): boolean {
return [agent.server_vendor, agent.server_product, agent.server_version].every(s => !isBlank(s))
}
}

export const discoverService = new DiscoverService()
1 change: 1 addition & 0 deletions packages/hawtio/src/plugins/connect/img/java-logo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit 3dbb373

Please sign in to comment.