Skip to content

Commit

Permalink
feat(springboot-plugin): Add Loggers tab with posibility to see,filte…
Browse files Browse the repository at this point in the history
…r and customize loggers
  • Loading branch information
mmelko committed Nov 16, 2023
1 parent 8dbc221 commit 3d825bf
Show file tree
Hide file tree
Showing 5 changed files with 314 additions and 10 deletions.
3 changes: 2 additions & 1 deletion packages/hawtio/src/plugins/logs/Logs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -454,9 +454,10 @@ const LogModal: React.FunctionComponent<{
)
}

const LogLevel: React.FunctionComponent<{ level: string }> = ({ level }) => {
export const LogLevel: React.FunctionComponent<{ level: string }> = ({ level }) => {
switch (level) {
case 'TRACE':
case 'OFF':
case 'DEBUG':
return <Label color='grey'>{level}</Label>
case 'INFO':
Expand Down
262 changes: 259 additions & 3 deletions packages/hawtio/src/plugins/springboot/Loggers.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,260 @@
import React from 'react'
import { PageSection } from '@patternfly/react-core'
import React, { useCallback, useEffect, useState } from 'react'
import {
Bullseye,
Button,
Dropdown,
DropdownItem,
DropdownToggle,
EmptyState,
EmptyStateBody,
EmptyStateIcon,
FormGroup,
PageSection,
Pagination,
SearchInput,
Toolbar,
ToolbarContent,
ToolbarFilter,
ToolbarGroup,
ToolbarItem,
} from '@patternfly/react-core'
import { configureLogLevel, getLoggerConfiguration } from '@hawtiosrc/plugins/springboot/springboot-service'
import { TableComposable, Tbody, Td, Th, Thead, Tr } from '@patternfly/react-table'
import { SearchIcon } from '@patternfly/react-icons'
import { Logger } from '@hawtiosrc/plugins/springboot/types'
import { LogLevel } from '@hawtiosrc/plugins/logs/Logs'

export const Loggers: React.FunctionComponent = () => <PageSection variant='light'>Loggers - TO BE DONE</PageSection>
const SetLogDropdown: React.FunctionComponent<{
currentLevel: string
loggerName: string
logLevels: string[]
setIsDropdownOpen: React.Dispatch<React.SetStateAction<string | null>>
isDropdownOpen: string | null
reloadLoggers: () => void
}> = ({ currentLevel, loggerName, logLevels, setIsDropdownOpen, isDropdownOpen, reloadLoggers }) => {
const items = logLevels.map(level => (
<DropdownItem
key={loggerName + '' + level}
onClick={() => {
configureLogLevel(loggerName, level)
reloadLoggers()
}}
>
<LogLevel level={level} />
</DropdownItem>
))

return (
<Dropdown
isPlain
onSelect={() => setIsDropdownOpen(null)}
defaultValue={currentLevel}
toggle={
<DropdownToggle
id={`toggle-basic-${loggerName}`}
onToggle={() => setIsDropdownOpen(prevState => (prevState === loggerName ? null : loggerName))}
>
<LogLevel level={currentLevel} />
</DropdownToggle>
}
isOpen={isDropdownOpen === loggerName}
dropdownItems={items}
/>
)
}

export const Loggers: React.FunctionComponent = () => {
const [searchTerm, setSearchTerm] = useState('')

const [filters, setFilters] = useState<string[]>([])
const [logLevel, setLogLevel] = useState<string>('ALL')
const [filteredLoggers, setFilteredLoggers] = useState<Logger[]>([])
const [loggers, setLoggers] = useState<Logger[]>([])
const [logLevels, setLogLevels] = useState<string[]>([])
const [isLogLevelDropdownOpen, setIsLogLevelDropdownOpen] = useState(false)
const [isDropdownOpen, setIsDropdownOpen] = useState<string | null>(null)
const [page, setPage] = useState(1)
const [perPage, setPerPage] = useState(20)
const [reloadLoggers, setReloadLoggers] = useState(false)

useEffect(() => {
getLoggerConfiguration().then(logConf => {
const sorted = logConf.loggers.sort((logger1, logger2) => {
if (logger1.name === 'ROOT') return -1
else if (logger2.name === 'ROOT') return 1
else return logger1.name.localeCompare(logger2.name)
})
setLoggers(sorted)
setLogLevels([...logConf.levels])
setFilteredLoggers(sorted)
})
}, [reloadLoggers])

const handleSearch = useCallback(
(value: string, logLevel: string, filters: string[]) => {
let filtered: Logger[] =
loggers.filter(logger => {
if (logLevel === 'ALL') {
if (value === '') {
return true
} else {
return logger.name.toLowerCase().includes(value.toLowerCase())
}
}
return logger.configuredLevel === logLevel && logger.name.toLowerCase().includes(value.toLowerCase())
}) ?? []

//filter with the rest of the filters
filters.forEach(value => {
filtered = filtered.filter(prop => prop.name.toLowerCase().includes(value.toLowerCase()))
})
setFilteredLoggers([...filtered])
},
[loggers],
)

useEffect(() => {
handleSearch(searchTerm, logLevel, filters)
setPage(1)
}, [filters, searchTerm, logLevel, loggers, handleSearch])

const onDeleteFilter = (filter: string) => {
const newFilters = filters.filter(f => f !== filter)
setFilters(newFilters)
}
const addToFilters = () => {
setFilters([...filters, searchTerm])
setSearchTerm('')
}
const clearFilters = () => {
setFilters([])
setSearchTerm('')
}

const PropsPagination = () => {
return (
<Pagination
itemCount={filteredLoggers.length}
page={page}
perPage={perPage}
onSetPage={(_evt, value) => setPage(value)}
onPerPageSelect={(_evt, value) => {
setPerPage(value)
setPage(1)
}}
variant='top'
/>
)
}
const getCurrentPage = (): Logger[] => {
const start = (page - 1) * perPage
const end = start + perPage
return filteredLoggers.slice(start, end)
}

const dropdownItems = ['ALL', ...logLevels].map(level => (
<DropdownItem
onClick={() => {
setLogLevel(level)
}}
key={level}
>
<LogLevel level={level} />
</DropdownItem>
))

const tableToolbar = (
<Toolbar>
<ToolbarContent>
<ToolbarGroup>
<Dropdown
data-testid='attribute-select'
onSelect={() => setIsLogLevelDropdownOpen(false)}
defaultValue='INFO'
toggle={
<DropdownToggle
data-testid='attribute-select-toggle'
id='toggle-basic'
onToggle={setIsLogLevelDropdownOpen}
>
<LogLevel level={logLevel} />
</DropdownToggle>
}
isOpen={isLogLevelDropdownOpen}
dropdownItems={dropdownItems}
/>
<ToolbarFilter
chips={filters}
deleteChip={(_e, filter) => onDeleteFilter(filter as string)}
deleteChipGroup={clearFilters}
categoryName='Filters'
>
<SearchInput
type='text'
data-testid='filter-input'
id='search-input'
placeholder='Search...'
value={searchTerm}
onChange={(_event, value) => setSearchTerm(value)}
aria-label='Search input'
/>
</ToolbarFilter>
<Button variant='secondary' onClick={addToFilters} isSmall>
Add Filter
</Button>
</ToolbarGroup>

<ToolbarItem variant='pagination'>
<PropsPagination />
</ToolbarItem>
</ToolbarContent>
</Toolbar>
)

return (
<PageSection variant='light'>
{tableToolbar}
{getCurrentPage().length > 0 && (
<FormGroup>
<TableComposable aria-label='Message Table' variant='compact' height='80vh' isStriped isStickyHeader>
<Thead>
<Tr>
<Th data-testid={'log-level-header'}>Log Level</Th>
<Th data-testid={'logger-name-header'}>Logger Name</Th>
</Tr>
</Thead>
<Tbody>
{getCurrentPage().map((logger, index) => {
return (
<Tr key={'row' + index} data-testid={'row' + index}>
<Td style={{ width: '20%' }}>
<SetLogDropdown
loggerName={logger.name}
currentLevel={logger.configuredLevel}
logLevels={logLevels}
setIsDropdownOpen={setIsDropdownOpen}
isDropdownOpen={isDropdownOpen}
reloadLoggers={() => {
setReloadLoggers(!reloadLoggers)
}}
/>
</Td>
<Td style={{ flex: 3 }}>{logger.name}</Td>
</Tr>
)
})}
</Tbody>
</TableComposable>
</FormGroup>
)}
{filteredLoggers.length === 0 && (
<Bullseye>
<EmptyState>
<EmptyStateIcon icon={SearchIcon} />
<EmptyStateBody>No results found.</EmptyStateBody>
</EmptyState>
</Bullseye>
)}
</PageSection>
)
}
39 changes: 38 additions & 1 deletion packages/hawtio/src/plugins/springboot/springboot-service.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { jolokiaService, workspace } from '@hawtiosrc/plugins'
import { HealthComponent, HealthData, JolokiaHealthData } from './types'
import { HealthComponent, HealthData, JolokiaHealthData, Logger, LoggerConfiguration } from './types'

export async function loadHealth(): Promise<HealthData> {
const data = (await jolokiaService.execute(
Expand Down Expand Up @@ -47,3 +47,40 @@ export async function getInfo() {
export async function hasEndpoint(name: string): Promise<boolean> {
return await workspace.treeContainsDomainAndProperties('org.springframework.boot', { type: 'Endpoint', name: name })
}

export async function getLoggerConfiguration() {
const data = (await jolokiaService.execute('org.springframework.boot:type=Endpoint,name=Loggers', 'loggers')) as {
loggers: {
[key: string]: {
configuredLevel?: string
effectiveLevel: string
}
}
levels: string[]
}

const loggers: Logger[] = []

Object.entries(data.loggers).forEach(([loggerName, loggerInfo]) => {
const logger: Logger = {
name: loggerName,
configuredLevel: loggerInfo['configuredLevel'] == null ? loggerInfo.effectiveLevel : loggerInfo.configuredLevel,
effectiveLevel: loggerInfo.effectiveLevel,
}
loggers.push(logger)
})

const loggerConfiguration: LoggerConfiguration = {
levels: data.levels,
loggers: loggers,
}

return loggerConfiguration
}

export async function configureLogLevel(loggerName: string, loggerLevel: string) {
return await jolokiaService.execute('org.springframework.boot:type=Endpoint,name=Loggers', 'configureLogLevel', [
loggerName,
loggerLevel,
])
}
10 changes: 10 additions & 0 deletions packages/hawtio/src/plugins/springboot/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,13 @@ export type JolokiaHealthData = {
}
}
}
export type LoggerConfiguration = {
levels: string[]
loggers: Logger[]
}

export type Logger = {
name: string
configuredLevel: string
effectiveLevel: string
}
10 changes: 5 additions & 5 deletions packages/hawtio/src/ui/notification/HawtioNotification.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,11 @@ export const HawtioNotification: React.FunctionComponent = () => {
const listener = (notification: Notification) => {
const key = getUniqueKey()
addAlert(notification.message, notification.type, key)
if (notification.duration) {
setTimeout(() => {
removeAlert(key)
}, notification.duration)
}
// if (notification.duration) {
// setTimeout(() => {
// // removeAlert(key)
// }, notification.duration)
// }
}
eventService.onNotify(listener)

Expand Down

0 comments on commit 3d825bf

Please sign in to comment.