Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: New grouped invocation logs view #29131

Merged
merged 17 commits into from
Feb 26, 2025
8 changes: 7 additions & 1 deletion frontend/src/scenes/pipeline/PipelineNode.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { ActivityScope, PipelineNodeTab, PipelineStage, PipelineTab } from '~/ty

import { BatchExportBackfills } from './BatchExportBackfills'
import { BatchExportRuns } from './BatchExportRuns'
import { HogFunctionLogs } from './hogfunctions/logs/HogFunctionLogs'
import { AppMetricsV2 } from './metrics/AppMetricsV2'
import { PipelineNodeConfiguration } from './PipelineNodeConfiguration'
import { pipelineNodeLogic, PipelineNodeLogicProps } from './pipelineNodeLogic'
Expand Down Expand Up @@ -80,7 +81,12 @@ export function PipelineNode(params: { stage?: string; id?: string } = {}): JSX.
) : (
<PipelineNodeMetrics id={id} />
),
[PipelineNodeTab.Logs]: <PipelineNodeLogs id={id} stage={stage} />,
[PipelineNodeTab.Logs]:
node.backend === PipelineBackend.HogFunction ? (
<HogFunctionLogs hogFunctionId={id.toString().substring(4)} />
) : (
<PipelineNodeLogs id={id} stage={stage} />
),
}
: {}

Expand Down
124 changes: 124 additions & 0 deletions frontend/src/scenes/pipeline/hogfunctions/logs/HogFunctionLogs.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import { IconEllipsis } from '@posthog/icons'
import { LemonButton, LemonMenu, LemonTag } from '@posthog/lemon-ui'
import { useActions, useValues } from 'kea'
import { LemonTableColumns } from 'lib/lemon-ui/LemonTable'
import { capitalizeFirstLetter } from 'lib/utils'
import { useMemo } from 'react'
import { urls } from 'scenes/urls'

import { hogFunctionLogsLogic } from './hogFunctionLogsLogic'
import { LogsViewer } from './LogsViewer'
import { GroupedLogEntry, LogsViewerLogicProps } from './logsViewerLogic'

const eventIdMatchers = [/Event: ([A-Za-z0-9-]+)/, /\/events\/([A-Za-z0-9-]+)\//]

export function HogFunctionLogs(props: { hogFunctionId: string }): JSX.Element {
return (
<LogsViewer
sourceType="hog_function"
sourceId={props.hogFunctionId}
renderColumns={(columns) => {
// Add in custom columns for handling retries
const newColumns: LemonTableColumns<GroupedLogEntry> = [
{
title: 'Status',
key: 'status',
width: 0,
render: (_, record) => (
<HogFunctionLogsStatus record={record} hogFunctionId={props.hogFunctionId} />
),
},
...columns.filter((column) => column.key !== 'logLevel'),
]

return newColumns
}}
/>
)
}

type HogFunctionLogsStatus = 'success' | 'failure' | 'running'

function HogFunctionLogsStatus({
record,
hogFunctionId,
}: {
record: GroupedLogEntry
hogFunctionId: string
}): JSX.Element {
const logicProps: LogsViewerLogicProps = {
sourceType: 'hog_function',
sourceId: hogFunctionId,
}

const { retries } = useValues(hogFunctionLogsLogic(logicProps))
const { retryInvocation } = useActions(hogFunctionLogsLogic(logicProps))

const thisRetry = retries[record.instanceId]

const status = useMemo<HogFunctionLogsStatus>((): HogFunctionLogsStatus => {
if (thisRetry === 'pending') {
return 'running'
}

const lastEntry = record.entries[record.entries.length - 1]

if (lastEntry.message.includes('Function completed') || lastEntry.message.includes('Execution successful')) {
return 'success'
}

if (lastEntry.level === 'ERROR') {
return 'failure'
}

return 'running'
}, [record, thisRetry])

const eventId = useMemo<string | undefined>(() => {
// TRICKY: We have the event ID in differnet logs. We will standardise this to be the invocation ID in the future.
const entryContainingEventId = record.entries.find(
(entry) => entry.message.includes('Function completed') || entry.message.includes('Suspending function')
)

if (!entryContainingEventId) {
return undefined
}

for (const matcher of eventIdMatchers) {
const match = entryContainingEventId.message.match(matcher)
if (match) {
return match[1]
}
}
}, [record])

return (
<div className="flex items-center gap-2">
<LemonTag type={status === 'success' ? 'success' : status === 'failure' ? 'danger' : 'warning'}>
{capitalizeFirstLetter(status)}
</LemonTag>

<LemonMenu
items={[
eventId
? {
label: 'View event',
to: urls.event(eventId, ''),
}
: null,
{
label: 'Retry event',
disabledReason: !eventId ? 'Could not find the source event' : undefined,
onClick: () => retryInvocation(record, eventId!),
},
]}
>
<LemonButton
size="xsmall"
icon={<IconEllipsis className="rotate-90" />}
loading={thisRetry === 'pending'}
/>
</LemonMenu>
</div>
)
}
Loading
Loading