Skip to content

Commit

Permalink
Add job logs, (#246)
Browse files Browse the repository at this point in the history
closes #210
closes #97
  • Loading branch information
felixmosh authored Mar 22, 2021
1 parent 672f654 commit eadd58c
Show file tree
Hide file tree
Showing 17 changed files with 163 additions and 52 deletions.
4 changes: 3 additions & 1 deletion example.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ const run = async () => {
exampleBull.process(async (job) => {
for (let i = 0; i <= 100; i++) {
await sleep(Math.random())
job.progress(i)
await job.progress(i)
await job.log(`Processing job at interval ${i}`)
if (Math.random() * 200 < 1) throw new Error(`Random error ${i}`)
}

Expand All @@ -42,6 +43,7 @@ const run = async () => {
for (let i = 0; i <= 100; i++) {
await sleep(Math.random())
await job.updateProgress(i)
await job.log(`Processing job at interval ${i}`)

if (Math.random() * 200 < 1) throw new Error(`Random error ${i}`)
}
Expand Down
3 changes: 3 additions & 0 deletions src/@types/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ export interface QueueAdapter {
): void

format(field: 'data' | 'returnValue', data: any): any

getJobLogs(jobId: string): Promise<string[]>
}

export interface QueueAdapterOptions {
Expand Down Expand Up @@ -92,6 +94,7 @@ export interface QueueActions {
promoteJob: (queueName: string) => (job: AppJob) => () => Promise<void>
retryJob: (queueName: string) => (job: AppJob) => () => Promise<void>
cleanJob: (queueName: string) => (job: AppJob) => () => Promise<void>
getJobLogs: (queueName: string) => (job: AppJob) => () => Promise<string[]>
retryAll: (queueName: string) => () => Promise<void>
cleanAllDelayed: (queueName: string) => () => Promise<void>
cleanAllFailed: (queueName: string) => () => Promise<void>
Expand Down
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { cleanAll } from './routes/cleanAll'
import { cleanJob } from './routes/cleanJob'
import { errorHandler } from './routes/errorHandler'
import { entryPoint } from './routes/index'
import { jobLogs } from './routes/jobLogs'
import { promoteJob } from './routes/promoteJob'

import { queuesHandler } from './routes/queues'
Expand Down Expand Up @@ -36,6 +37,7 @@ router.put('/api/queues/:queueName/retry', wrapAsync(retryAll))
router.put('/api/queues/:queueName/:id/retry', wrapAsync(retryJob))
router.put('/api/queues/:queueName/:id/clean', wrapAsync(cleanJob))
router.put('/api/queues/:queueName/:id/promote', wrapAsync(promoteJob))
router.get('/api/queues/:queueName/:id/logs', wrapAsync(jobLogs))
router.put('/api/queues/:queueName/clean/:queueStatus', wrapAsync(cleanAll))
router.use(errorHandler)

Expand Down
2 changes: 2 additions & 0 deletions src/queueAdapters/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ export abstract class BaseAdapter implements QueueAdapter {
end?: number,
): Promise<(Job | JobMq)[]>

public abstract getJobLogs(id: string): Promise<string[]>

public abstract getName(): string

public abstract getClient(): Promise<Redis.Redis>
Expand Down
7 changes: 5 additions & 2 deletions src/queueAdapters/bull.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,11 @@ import {
JobCleanStatus,
JobCounts,
JobStatus,
QueueAdapter,
QueueAdapterOptions,
} from '../@types/app'
import { BaseAdapter } from './base'

export class BullAdapter extends BaseAdapter implements QueueAdapter {
export class BullAdapter extends BaseAdapter {
constructor(public queue: Queue, options: Partial<QueueAdapterOptions> = {}) {
super(options)
}
Expand Down Expand Up @@ -41,4 +40,8 @@ export class BullAdapter extends BaseAdapter implements QueueAdapter {
public getJobCounts(..._jobStatuses: JobStatus[]): Promise<JobCounts> {
return (this.queue.getJobCounts() as unknown) as Promise<JobCounts>
}

public getJobLogs(id: string): Promise<string[]> {
return this.queue.getJobLogs(id).then(({ logs }) => logs)
}
}
4 changes: 4 additions & 0 deletions src/queueAdapters/bullMQ.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,8 @@ export class BullMQAdapter extends BaseAdapter {
...jobStatuses,
) as unknown) as Promise<JobCounts>
}

public getJobLogs(id: string): Promise<string[]> {
return this.queue.getJobLogs(id).then(({ logs }) => logs)
}
}
28 changes: 28 additions & 0 deletions src/routes/jobLogs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { Request, RequestHandler, Response } from 'express-serve-static-core'
import { BullBoardQueues } from '../@types/app'

export const jobLogs: RequestHandler = async (req: Request, res: Response) => {
const { bullBoardQueues } = req.app.locals as {
bullBoardQueues: BullBoardQueues
}
const { queueName, id } = req.params
const { queue } = bullBoardQueues[queueName]

if (!queue) {
return res.status(404).send({
error: 'Queue not found',
})
}

const job = await queue.getJob(id)

if (!job) {
return res.status(404).send({
error: 'Job not found',
})
}

const logs = await queue.getJobLogs(id)

return res.json(logs)
}
1 change: 1 addition & 0 deletions src/ui/components/JobCard/Details/Details.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@

.tabContent > div {
overflow: auto;
padding-bottom: 2rem;
height: 100%;
}

Expand Down
14 changes: 11 additions & 3 deletions src/ui/components/JobCard/Details/Details.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,16 @@ import { useDetailsTabs } from '../../../hooks/useDetailsTabs'
import { Status } from '../../constants'
import { Button } from '../Button/Button'
import s from './Details.module.css'
import { DetailsContent } from './DetailsContent/DetailsContent'

interface DetailsProps {
job: AppJob
status: Status
actions: { getJobLogs: () => Promise<string[]> }
}

export const Details = ({ status, job }: DetailsProps) => {
const { tabs, getTabContent } = useDetailsTabs(status)
export const Details = ({ status, job, actions }: DetailsProps) => {
const { tabs, selectedTab } = useDetailsTabs(status)

if (tabs.length === 0) {
return null
Expand All @@ -29,7 +31,13 @@ export const Details = ({ status, job }: DetailsProps) => {
))}
</ul>
<div className={s.tabContent}>
<div>{getTabContent(job)}</div>
<div>
<DetailsContent
selectedTab={selectedTab}
job={job}
actions={actions}
/>
</div>
</div>
</div>
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import React from 'react'
import { AppJob } from '../../../../../@types/app'
import { TabsType } from '../../../../hooks/useDetailsTabs'
import { Highlight } from '../../../Highlight/Highlight'
import { JobLogs } from './JobLogs/JobLogs'

interface DetailsContentProps {
job: AppJob
selectedTab: TabsType
actions: {
getJobLogs: () => Promise<string[]>
}
}

export const DetailsContent = ({
selectedTab,
job: { stacktrace, data, returnValue, opts, failedReason },
actions,
}: DetailsContentProps) => {
switch (selectedTab) {
case 'Data':
return (
<Highlight language="json">
{JSON.stringify({ data, returnValue }, null, 2)}
</Highlight>
)
case 'Options':
return (
<Highlight language="json">{JSON.stringify(opts, null, 2)}</Highlight>
)
case 'Error':
return (
<>
{stacktrace.length === 0 ? (
<div className="error">{!!failedReason ? failedReason : 'NA'}</div>
) : (
<Highlight language="stacktrace" key="stacktrace">
{stacktrace}
</Highlight>
)}
</>
)
case 'Logs':
return <JobLogs actions={actions} />
default:
return null
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.jobLogs {
margin: 0;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import React, { useEffect, useState } from 'react'
import s from './JobLogs.module.css'

interface JobLogsProps {
actions: {
getJobLogs: () => Promise<string[]>
}
}

export const JobLogs = ({ actions }: JobLogsProps) => {
const [logs, setLogs] = useState<string[]>([])

useEffect(() => {
actions.getJobLogs().then((logs) => setLogs(logs))
}, [])

if (!Array.isArray(logs) || !logs.length) {
return null
}

return (
<ul className={s.jobLogs}>
{logs.map((log, idx) => (
<li key={idx}>{log}</li>
))}
</ul>
)
}
3 changes: 2 additions & 1 deletion src/ui/components/JobCard/JobCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ interface JobCardProps {
promoteJob: () => Promise<void>
retryJob: () => Promise<void>
cleanJob: () => Promise<void>
getJobLogs: () => Promise<string[]>
}
}

Expand All @@ -38,7 +39,7 @@ export const JobCard = ({
{!readOnlyMode && <JobActions status={status} actions={actions} />}
</div>
<div className={s.content}>
<Details status={status} job={job} />
<Details status={status} job={job} actions={actions} />
{typeof job.progress === 'number' && (
<Progress
percentage={job.progress}
Expand Down
1 change: 1 addition & 0 deletions src/ui/components/QueuePage/QueuePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export const QueuePage = ({
cleanJob: actions.cleanJob(queue?.name)(job),
promoteJob: actions.promoteJob(queue?.name)(job),
retryJob: actions.retryJob(queue?.name)(job),
getJobLogs: actions.getJobLogs(queue?.name)(job),
}}
readOnlyMode={queue?.readOnlyMode}
/>
Expand Down
52 changes: 8 additions & 44 deletions src/ui/hooks/useDetailsTabs.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
import React, { useEffect, useState } from 'react'
import { AppJob } from '../../@types/app'
import { useEffect, useState } from 'react'
import { Status, STATUSES } from '../components/constants'
import { Highlight } from '../components/Highlight/Highlight'

const regularItems = ['Data', 'Options']
const regularItems = ['Data', 'Options', 'Logs'] as const

export type TabsType = typeof regularItems[number] | 'Error'

export function useDetailsTabs(currentStatus: Status) {
const [tabs, updateTabs] = useState<string[]>([])
const [tabs, updateTabs] = useState<TabsType[]>([])
const [selectedTabIdx, setSelectedTabIdx] = useState(0)
const selectedTab = tabs[selectedTabIdx]

useEffect(() => {
updateTabs(
(currentStatus === STATUSES.failed ? ['Error'] : []).concat(regularItems),
currentStatus === STATUSES.failed
? ['Error', ...regularItems]
: [...regularItems],
)
}, [currentStatus])

Expand All @@ -23,43 +25,5 @@ export function useDetailsTabs(currentStatus: Status) {
selectTab: () => setSelectedTabIdx(index),
})),
selectedTab,
getTabContent: ({
data,
returnValue,
opts,
failedReason,
stacktrace,
}: AppJob) => {
switch (selectedTab) {
case 'Data':
return (
<Highlight language="json">
{JSON.stringify({ data, returnValue }, null, 2)}
</Highlight>
)
case 'Options':
return (
<Highlight language="json">
{JSON.stringify(opts, null, 2)}
</Highlight>
)
case 'Error':
return (
<>
{stacktrace.length === 0 ? (
<div className="error">
{!!failedReason ? failedReason : 'NA'}
</div>
) : (
<Highlight language="stacktrace" key="stacktrace">
{stacktrace}
</Highlight>
)}
</>
)
default:
return null
}
},
}
}
4 changes: 4 additions & 0 deletions src/ui/hooks/useStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,9 @@ export const useStore = (api: Api): Store => {
const cleanAllCompleted = (queueName: string) => () =>
api.cleanAllCompleted(queueName).then(update)

const getJobLogs = (queueName: string) => (job: AppJob) => () =>
api.getJobLogs(queueName, job.id)

return {
state,
actions: {
Expand All @@ -96,6 +99,7 @@ export const useStore = (api: Api): Store => {
cleanAllDelayed,
cleanAllFailed,
cleanAllCompleted,
getJobLogs,
setSelectedStatuses,
},
selectedStatuses,
Expand Down
11 changes: 10 additions & 1 deletion src/ui/services/Api.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import Axios, { AxiosInstance, AxiosResponse } from 'axios'
import { toast } from 'react-toastify'
import { GetQueues } from '../../@types/api'
import { SelectedStatuses } from '../../@types/app'
import { toast } from 'react-toastify'

export class Api {
private axios: AxiosInstance
Expand Down Expand Up @@ -68,6 +68,15 @@ export class Api {
)
}

public getJobLogs(
queueName: string,
jobId: string | number | undefined,
): Promise<string[]> {
return this.axios.get(
`/queues/${encodeURIComponent(queueName)}/${jobId}/logs`,
)
}

private handleResponse(response: AxiosResponse): any {
return response.data
}
Expand Down

0 comments on commit eadd58c

Please sign in to comment.