Skip to content

Commit

Permalink
Merge pull request #367 from duyet/feat/overview
Browse files Browse the repository at this point in the history
feat: add charts to failed-query page
  • Loading branch information
duyet authored Sep 25, 2024
2 parents e7a27d5 + 4db2111 commit 4be031a
Show file tree
Hide file tree
Showing 5 changed files with 255 additions and 50 deletions.
127 changes: 85 additions & 42 deletions app/[host]/[query]/queries/failed-queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,45 +3,46 @@ import { type QueryConfig } from '@/types/query-config'

export const failedQueriesConfig: QueryConfig = {
name: 'failed-queries',
description: "type IN ['ExceptionBeforeStart', 'ExceptionWhileProcessing']",
sql: `
SELECT
type,
query_start_time,
query_duration_ms,
query_id,
query_kind,
is_initial_query,
normalizeQuery(query) AS normalized_query,
concat(toString(read_rows), ' rows / ', formatReadableSize(read_bytes)) AS read,
concat(toString(written_rows), ' rows / ', formatReadableSize(written_bytes)) AS written,
concat(toString(result_rows), ' rows / ', formatReadableSize(result_bytes)) AS result,
formatReadableSize(memory_usage) AS memory_usage,
exception,
concat('\n', stack_trace) AS stack_trace,
user,
initial_user,
multiIf(empty(client_name), http_user_agent, concat(client_name, ' ', toString(client_version_major), '.', toString(client_version_minor), '.', toString(client_version_patch))) AS client,
client_hostname,
databases,
tables,
columns,
used_aggregate_functions,
used_aggregate_function_combinators,
used_database_engines,
used_data_type_families,
used_dictionaries,
used_formats,
used_functions,
used_storages,
used_table_functions,
thread_ids,
ProfileEvents,
Settings
FROM system.query_log
WHERE type IN ['ExceptionBeforeStart', 'ExceptionWhileProcessing']
ORDER BY query_start_time DESC
LIMIT 100
`,
SELECT
type,
query_start_time,
query_duration_ms,
query_id,
query_kind,
is_initial_query,
normalizeQuery(query) AS normalized_query,
concat(toString(read_rows), ' rows / ', formatReadableSize(read_bytes)) AS read,
concat(toString(written_rows), ' rows / ', formatReadableSize(written_bytes)) AS written,
concat(toString(result_rows), ' rows / ', formatReadableSize(result_bytes)) AS result,
formatReadableSize(memory_usage) AS memory_usage,
exception,
concat('\n', stack_trace) AS stack_trace,
user,
initial_user,
multiIf(empty(client_name), http_user_agent, concat(client_name, ' ', toString(client_version_major), '.', toString(client_version_minor), '.', toString(client_version_patch))) AS client,
client_hostname,
toString(databases) AS databases,
toString(tables) AS tables,
toString(columns) AS columns,
toString(used_aggregate_functions) AS used_aggregate_functions,
toString(used_aggregate_function_combinators) AS used_aggregate_function_combinators,
toString(used_database_engines) AS used_database_engines,
toString(used_data_type_families) AS used_data_type_families,
toString(used_dictionaries) AS used_dictionaries,
toString(used_formats) AS used_formats,
toString(used_functions) AS used_functions,
toString(used_storages) AS used_storages,
toString(used_table_functions) AS used_table_functions,
toString(thread_ids) AS thread_ids,
ProfileEvents,
Settings
FROM system.query_log
WHERE type IN ['ExceptionBeforeStart', 'ExceptionWhileProcessing']
ORDER BY query_start_time DESC
LIMIT 1000
`,
columns: [
'normalized_query',
'exception',
Expand Down Expand Up @@ -75,14 +76,56 @@ export const failedQueriesConfig: QueryConfig = {
'thread_ids',
],
columnFormats: {
normalized_query: ColumnFormat.Code,
normalized_query: [
ColumnFormat.CodeDialog,
{
trigger_classname: 'w-80 line-clamp-4',
dialog_classname: 'max-w-screen-xl',
max_truncate: 200,
},
],
type: ColumnFormat.ColoredBadge,
query_duration_ms: ColumnFormat.Duration,
query_start_time: ColumnFormat.RelatedTime,
exception: ColumnFormat.CodeDialog,
stack_trace: ColumnFormat.CodeToggle,
client: ColumnFormat.Code,
exception: [
ColumnFormat.CodeDialog,
{ trigger_classname: 'w-80 line-clamp-2' },
],
stack_trace: ColumnFormat.CodeDialog,
client: ColumnFormat.CodeDialog,
user: ColumnFormat.ColoredBadge,
initial_user: ColumnFormat.ColoredBadge,
is_initial_query: ColumnFormat.Boolean,
query_kind: ColumnFormat.Badge,
databases: ColumnFormat.CodeDialog,
tables: ColumnFormat.CodeDialog,
columns: ColumnFormat.CodeDialog,
used_aggregate_functions: ColumnFormat.CodeDialog,
used_aggregate_function_combinators: ColumnFormat.CodeDialog,
used_formats: ColumnFormat.CodeDialog,
used_dictionaries: ColumnFormat.CodeDialog,
used_functions: ColumnFormat.CodeDialog,
used_table_functions: ColumnFormat.CodeDialog,
thread_ids: ColumnFormat.CodeDialog,
},
relatedCharts: [
[
'failed-query-count',
{
title: 'Failed Queries over last 14 days',
interval: 'toStartOfHour',
lastHours: 24 * 14,
showLegend: false,
},
],
[
'failed-query-count-by-user',
{
title: 'Failed Queries over last 14 days by Users',
interval: 'toStartOfHour',
lastHours: 24 * 14,
showLegend: false,
},
],
],
}
71 changes: 71 additions & 0 deletions components/charts/failed-query-count-by-user.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { type ChartProps } from '@/components/charts/chart-props'
import { BarChart } from '@/components/generic-charts/bar'
import { ChartCard } from '@/components/generic-charts/chart-card'
import { fetchData } from '@/lib/clickhouse'
import { applyInterval } from '@/lib/clickhouse-query'

export async function ChartFailedQueryCountByType({
title,
interval = 'toStartOfDay',
lastHours = 24 * 14,
className,
chartClassName,
...props
}: ChartProps) {
const query = `
SELECT ${applyInterval(interval, 'event_time')},
user,
countDistinct(query_id) AS count
FROM merge(system, '^query_log')
WHERE
type IN ['ExceptionBeforeStart', 'ExceptionWhileProcessing']
AND event_time >= (now() - INTERVAL ${lastHours} HOUR)
GROUP BY 1, 2
ORDER BY
1 ASC,
3 DESC
`
const { data: raw } = await fetchData<
{
event_time: string
user: string
count: number
}[]
>({ query })

const data = raw.reduce(
(acc, cur) => {
const { event_time, user, count } = cur
if (acc[event_time] === undefined) {
acc[event_time] = {}
}
acc[event_time][user] = count
return acc
},
{} as Record<string, Record<string, number>>
)

const barData = Object.entries(data).map(([event_time, obj]) => {
return { event_time, ...obj }
})

const users = Object.values(data).reduce((acc, cur) => {
return Array.from(new Set([...acc, ...Object.keys(cur)]))
}, [] as string[])

return (
<ChartCard title={title} className={className} sql={query} data={barData}>
<BarChart
className={chartClassName}
data={barData}
index="event_time"
categories={users}
showLegend
stack
{...props}
/>
</ChartCard>
)
}

export default ChartFailedQueryCountByType
91 changes: 91 additions & 0 deletions components/charts/failed-query-count.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { type ChartProps } from '@/components/charts/chart-props'
import { AreaChart } from '@/components/generic-charts/area'
import { ChartCard } from '@/components/generic-charts/chart-card'
import { fetchData } from '@/lib/clickhouse'
import { cn } from '@/lib/utils'

export async function ChartFailedQueryCount({
title,
interval = 'toStartOfMinute',
className,
chartClassName,
chartCardContentClassName,
lastHours = 24,
showXAxis = true,
showLegend = false,
showCartesianGrid = true,
breakdown = 'breakdown',
...props
}: ChartProps) {
const query = `
WITH event_count AS (
SELECT ${interval}(event_time) AS event_time,
COUNT() AS query_count
FROM merge(system, '^query_log')
WHERE
type IN ['ExceptionBeforeStart', 'ExceptionWhileProcessing']
AND event_time >= (now() - INTERVAL ${lastHours} HOUR)
GROUP BY 1
ORDER BY 1
),
query_type AS (
SELECT ${interval}(event_time) AS event_time,
type AS query_type,
COUNT() AS count
FROM merge(system, '^query_log')
WHERE
type IN ['ExceptionBeforeStart', 'ExceptionWhileProcessing']
AND event_time >= (now() - INTERVAL ${lastHours} HOUR)
GROUP BY 1, 2
ORDER BY 3 DESC
),
breakdown AS (
SELECT event_time,
groupArray((query_type, count)) AS breakdown
FROM query_type
GROUP BY 1
)
SELECT event_time,
query_count,
breakdown.breakdown AS breakdown
FROM event_count
LEFT JOIN breakdown USING event_time
ORDER BY 1
`
const { data } = await fetchData<
{
event_time: string
query_count: number
breakdown: Array<[string, number] | Record<string, string>>
}[]
>({ query })

return (
<ChartCard
title={title}
className={className}
contentClassName={chartCardContentClassName}
sql={query}
data={data}
>
<AreaChart
className={cn('h-52', chartClassName)}
data={data}
index="event_time"
categories={['query_count']}
readable="quantity"
stack
showLegend={showLegend}
showXAxis={showXAxis}
showCartesianGrid={showCartesianGrid}
colors={['--chart-1']}
breakdown={breakdown}
breakdownLabel="query_type"
breakdownValue="count"
{...props}
/>
</ChartCard>
)
}

export default ChartFailedQueryCount
8 changes: 4 additions & 4 deletions components/data-table/cells/code-dialog-format.cy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,22 @@ describe('<CodeDialogFormat />', () => {
const shortCode = 'SELECT * FROM table'
cy.mount(<CodeDialogFormat value={shortCode} />)
cy.get('code').should('contain.text', shortCode)
cy.get('button svg[role="open-dialog"]').should('not.exist')
cy.get('svg[role="open-dialog"]').should('not.exist')
})

it('renders long code with dialog', () => {
const longCode =
'SELECT * FROM table WHERE column1 = "value" AND column2 > 100 ORDER BY column3 DESC LIMIT 10'
cy.mount(<CodeDialogFormat value={longCode} />)
cy.get('code').should('exist')
cy.get('button svg[role="open-dialog"]').should('exist')
cy.get('svg[role="open-dialog"]').should('exist')
})

it('opens dialog on button click', () => {
const longCode =
'SELECT * FROM table WHERE column1 = "value" AND column2 > 100 ORDER BY column3 DESC LIMIT 10'
cy.mount(<CodeDialogFormat value={longCode} />)
cy.get('button svg[role="open-dialog"]').click()
cy.get('svg[role="open-dialog"]').click()
cy.get('div[role="dialog"]').should('be.visible')
cy.get('div[role="dialog"] code').should('contain.text', longCode)
})
Expand All @@ -35,7 +35,7 @@ describe('<CodeDialogFormat />', () => {
hide_query_comment: true,
}
cy.mount(<CodeDialogFormat value={longCode} options={options} />)
cy.get('button svg[role="open-dialog"]').click()
cy.get('svg[role="open-dialog"]').click()
cy.get('div[role="dialog"]').within(() => {
cy.contains('Custom Title').should('be.visible')
cy.contains('Custom Description').should('be.visible')
Expand Down
8 changes: 4 additions & 4 deletions lib/clickhouse-query.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,21 @@ import { applyInterval } from './clickhouse-query'
describe('applyInterval', () => {
it('should apply toStartOfDay for toStartOfDay interval', () => {
const result = applyInterval('toStartOfDay', 'myColumn', 'myAlias')
expect(result).toEqual('toDate(toStartOfDay(myColumn)) as myAlias') // Change to toEqual
expect(result).toEqual('toDate(toStartOfDay(myColumn)) AS myAlias') // Change to toEqual
})

it('should apply toStartOfWeek for toStartOfWeek interval', () => {
const result = applyInterval('toStartOfWeek', 'myColumn')
expect(result).toEqual('toDate(toStartOfWeek(myColumn)) as myColumn') // Change to toEqual
expect(result).toEqual('toDate(toStartOfWeek(myColumn)) AS myColumn') // Change to toEqual
})

it('should apply toStartOfMonth for toStartOfMonth interval', () => {
const result = applyInterval('toStartOfMonth', 'myColumn', 'myAlias')
expect(result).toEqual('toDate(toStartOfMonth(myColumn)) as myAlias') // Change to toEqual
expect(result).toEqual('toDate(toStartOfMonth(myColumn)) AS myAlias') // Change to toEqual
})

it('should apply toStartOfHour for other intervals', () => {
const result = applyInterval('toStartOfHour', 'myColumn', 'myAlias')
expect(result).toEqual('toStartOfHour(myColumn) as myAlias') // Change to toEqual
expect(result).toEqual('toStartOfHour(myColumn) AS myAlias') // Change to toEqual
})
})

1 comment on commit 4be031a

@vercel
Copy link

@vercel vercel bot commented on 4be031a Sep 25, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.