Skip to content

Commit

Permalink
feat(logs): implement Log modal to view log details #153
Browse files Browse the repository at this point in the history
Fix #153
  • Loading branch information
tadayosi committed Sep 4, 2023
1 parent 02eea31 commit e8fbf47
Show file tree
Hide file tree
Showing 5 changed files with 195 additions and 52 deletions.
193 changes: 176 additions & 17 deletions packages/hawtio/src/plugins/logs/Logs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,19 @@ import {
Bullseye,
Button,
Card,
CardBody,
CardTitle,
CodeBlock,
CodeBlockCode,
DescriptionList,
DescriptionListDescription,
DescriptionListGroup,
DescriptionListTerm,
EmptyState,
EmptyStateBody,
EmptyStateIcon,
Label,
Modal,
PageSection,
Pagination,
PaginationProps,
Expand All @@ -24,9 +33,9 @@ import {
import { SearchIcon } from '@patternfly/react-icons'
import { TableComposable, Tbody, Td, Th, Thead, Tr } from '@patternfly/react-table'
import React, { useEffect, useRef, useState } from 'react'
import { log } from './globals'
import { LogEntry } from './log-entry'
import { LOGS_UPDATE_INTERVAL, LogFilter, logsService } from './logs-service'
import { log } from './globals'

export const Logs: React.FunctionComponent = () => {
return (
Expand Down Expand Up @@ -56,6 +65,10 @@ const LogsTable: React.FunctionComponent = () => {
const [perPage, setPerPage] = useState(10)
const [paginatedLogs, setPaginatedLogs] = useState(filteredLogs.slice(0, perPage))

// Modal
const [isModalOpen, setIsModalOpen] = useState(false)
const [selected, setSelected] = useState<LogEntry | null>(null)

useEffect(() => {
const loadLogs = async () => {
const result = await logsService.loadLogs()
Expand Down Expand Up @@ -223,20 +236,13 @@ const LogsTable: React.FunctionComponent = () => {
</Toolbar>
)

const renderLabel = (level: string) => {
switch (level) {
case 'TRACE':
case 'DEBUG':
return <Label color='grey'>{level}</Label>
case 'INFO':
return <Label color='blue'>{level}</Label>
case 'WARN':
return <Label color='orange'>{level}</Label>
case 'ERROR':
return <Label color='red'>{level}</Label>
default:
return level
}
const selectLog = (log: LogEntry) => {
setSelected(log)
handleModalToggle()
}

const handleModalToggle = () => {
setIsModalOpen(!isModalOpen)
}

return (
Expand All @@ -253,9 +259,11 @@ const LogsTable: React.FunctionComponent = () => {
</Thead>
<Tbody>
{paginatedLogs.map((log, index) => (
<Tr key={index}>
<Tr key={index} onRowClick={() => selectLog(log)}>
<Td dataLabel='timestamp'>{log.getTimestamp()}</Td>
<Td dataLabel='level'>{renderLabel(log.event.level)}</Td>
<Td dataLabel='level'>
<LogLevel level={log.event.level} />
</Td>
<Td dataLabel='logger'>{log.event.logger}</Td>
<Td dataLabel='message'>{log.event.message}</Td>
</Tr>
Expand All @@ -281,6 +289,157 @@ const LogsTable: React.FunctionComponent = () => {
</Tbody>
</TableComposable>
{renderPagination('bottom', false)}
<LogModal isOpen={isModalOpen} onClose={handleModalToggle} log={selected} />
</Card>
)
}

const LogModal: React.FunctionComponent<{
isOpen: boolean
onClose: () => void
log: LogEntry | null
}> = ({ isOpen, onClose, log }) => {
if (!log) {
return null
}

const { event } = log

const logDetails = (
<Card isCompact isPlain>
<CardBody>
<DescriptionList isCompact isHorizontal>
<DescriptionListGroup>
<DescriptionListTerm>Timestamp</DescriptionListTerm>
<DescriptionListDescription>{log.getTimestamp()}</DescriptionListDescription>
</DescriptionListGroup>
<DescriptionListGroup>
<DescriptionListTerm>Level</DescriptionListTerm>
<DescriptionListDescription>
<LogLevel level={event.level} />
</DescriptionListDescription>
</DescriptionListGroup>
<DescriptionListGroup>
<DescriptionListTerm>Logger</DescriptionListTerm>
<DescriptionListDescription>{event.logger}</DescriptionListDescription>
</DescriptionListGroup>
{log.hasLogSourceLineHref && (
<React.Fragment>
<DescriptionListGroup>
<DescriptionListTerm>Class</DescriptionListTerm>
<DescriptionListDescription>{event.className}</DescriptionListDescription>
</DescriptionListGroup>
<DescriptionListGroup>
<DescriptionListTerm>Method</DescriptionListTerm>
<DescriptionListDescription>{event.methodName}</DescriptionListDescription>
</DescriptionListGroup>
<DescriptionListGroup>
<DescriptionListTerm>File</DescriptionListTerm>
<DescriptionListDescription>
{event.fileName}:{event.lineNumber}
</DescriptionListDescription>
</DescriptionListGroup>
</React.Fragment>
)}
{event.host && (
<DescriptionListGroup>
<DescriptionListTerm>Host</DescriptionListTerm>
<DescriptionListDescription>{event.host}</DescriptionListDescription>
</DescriptionListGroup>
)}
<DescriptionListGroup>
<DescriptionListTerm>Thread</DescriptionListTerm>
<DescriptionListDescription>{event.thread}</DescriptionListDescription>
</DescriptionListGroup>
<DescriptionListGroup>
<DescriptionListTerm>Message</DescriptionListTerm>
<DescriptionListDescription>
<CodeBlock>
<CodeBlockCode>{event.message}</CodeBlockCode>
</CodeBlock>
</DescriptionListDescription>
</DescriptionListGroup>
{event.exception && (
<DescriptionListGroup>
<DescriptionListTerm>Stack Trace</DescriptionListTerm>
<DescriptionListDescription>{event.exception}</DescriptionListDescription>
</DescriptionListGroup>
)}
</DescriptionList>
</CardBody>
</Card>
)

const osgiProperties = log.hasOSGiProperties && (
<Card isCompact isPlain>
<CardTitle>OSGi Properties</CardTitle>
<CardBody>
<DescriptionList isCompact isHorizontal>
{[
{ key: 'bundle.name', name: 'Bundle Name' },
{ key: 'bundle.id', name: 'Bundle ID' },
{ key: 'bundle.version', name: 'Bundle Version' },
]
.filter(({ key }) => event.properties[key] !== undefined)
.map(({ key, name }) => (
<DescriptionListGroup key={key}>
<DescriptionListTerm>${name}</DescriptionListTerm>
<DescriptionListDescription>{event.properties[key]}</DescriptionListDescription>
</DescriptionListGroup>
))}
</DescriptionList>
</CardBody>
</Card>
)

const mdcProperties = log.hasMDCProperties && (
<Card isCompact isPlain>
<CardTitle>MDC Properties</CardTitle>
<CardBody>
<DescriptionList isCompact isHorizontal>
{Object.entries(log.mdcProperties).map(([key, value]) => (
<DescriptionListGroup key={key}>
<DescriptionListTerm>{key}</DescriptionListTerm>
<DescriptionListDescription>{value}</DescriptionListDescription>
</DescriptionListGroup>
))}
</DescriptionList>
</CardBody>
</Card>
)

return (
<Modal
id='logs-log-modal'
variant='large'
title='Log'
isOpen={isOpen}
onClose={onClose}
actions={[
<Button key='close' onClick={onClose}>
Close
</Button>,
]}
>
{logDetails}
{osgiProperties}
{mdcProperties}
</Modal>
)
}

const LogLevel: React.FunctionComponent<{ level: string }> = ({ level }) => {
switch (level) {
case 'TRACE':
case 'DEBUG':
return <Label color='grey'>{level}</Label>
case 'INFO':
return <Label color='blue'>{level}</Label>
case 'WARN':
return <Label color='orange'>{level}</Label>
case 'ERROR':
return <Label color='red'>{level}</Label>
default:
return level
}
}
4 changes: 2 additions & 2 deletions packages/hawtio/src/plugins/logs/log-entry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,9 @@ describe('LogEntry', () => {
const resultMDC = new LogEntry(eventMDC)

// then
expect(resultNoMDC.hasMDCProps).toBe(false)
expect(resultNoMDC.hasMDCProperties).toBe(false)
expect(resultNoMDC.mdcProperties).toEqual({})
expect(resultMDC.hasMDCProps).toBe(true)
expect(resultMDC.hasMDCProperties).toBe(true)
expect(resultMDC.mdcProperties).toEqual({
'custom.key1': 'custom.value1',
'custom.key2': 'custom.value2',
Expand Down
47 changes: 17 additions & 30 deletions packages/hawtio/src/plugins/logs/log-entry.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { isEmpty } from '@hawtiosrc/util/objects'

export type LogEvent = {
seq: number
timestamp: string
Expand All @@ -8,49 +10,34 @@ export type LogEvent = {

properties: Record<string, string>

className?: string
containerName?: string
exception?: string
fileName?: string
host?: string
lineNumber?: number
methodName?: string
thread?: string
className: string | null
containerName: string | null
exception: string | null
fileName: string | null
host: string | null
lineNumber: number | null
methodName: string | null
thread: string | null
}

export class LogEntry {
hasOSGiProps: boolean
hasMDCProps: boolean
hasOSGiProperties: boolean
hasMDCProperties: boolean
hasLogSourceHref: boolean
hasLogSourceLineHref: boolean
levelClass: string
logSourceUrl: string
mdcProperties: Record<string, string>

constructor(readonly event: LogEvent) {
this.hasOSGiProps = LogEntry.hasOSGiProps(event.properties)
this.hasOSGiProperties = LogEntry.hasOSGiProperties(event.properties)
this.hasLogSourceHref = LogEntry.hasLogSourceHref(event.properties)
this.hasLogSourceLineHref = event.lineNumber !== undefined
this.levelClass = LogEntry.getLevelClass(event.level)
this.hasLogSourceLineHref = event.lineNumber !== null
this.logSourceUrl = LogEntry.getLogSourceUrl(event)
this.mdcProperties = LogEntry.mdcProperties(event.properties)
this.hasMDCProps = Object.keys(this.mdcProperties).length !== 0
}

private static getLevelClass(level: string): string {
switch (level) {
case 'INFO':
return 'text-info'
case 'WARN':
return 'text-warning'
case 'ERROR':
return 'text-danger'
default:
return ''
}
this.hasMDCProperties = !isEmpty(this.mdcProperties)
}

private static hasOSGiProps(properties: Record<string, string>): boolean {
private static hasOSGiProperties(properties: Record<string, string>): boolean {
return Object.keys(properties).some(key => key.indexOf('bundle') === 0)
}

Expand All @@ -60,7 +47,7 @@ export class LogEntry {

// TODO: need this?
private static getLogSourceUrl(event: LogEvent): string {
const removeQuestion = (s?: string) => (s && s !== '?' ? s : null)
const removeQuestion = (s: string | null) => (s && s !== '?' ? s : null)
const fileName = removeQuestion(event.fileName)
const className = removeQuestion(event.className)
const mavenCoords = event.properties['maven.coordinates']
Expand Down
1 change: 0 additions & 1 deletion packages/hawtio/src/plugins/shared/tree/node.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { domainNodeType } from '@hawtiosrc/plugins/camel/globals'
import { workspace } from '../workspace'
import { Icons, MBeanNode, PropertyList } from './node'
import { MBeanTree } from './tree'
Expand Down
2 changes: 0 additions & 2 deletions packages/hawtio/src/util/htmls.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import { isString } from './objects'

/**
* Escapes only tags ('<' and '>') as opposed to typical URL encodings.
*
Expand Down

0 comments on commit e8fbf47

Please sign in to comment.