Skip to content

Commit f3a3751

Browse files
committed
feat: LogEventChart timestamp filling, chart query bugfix for filters
1 parent 1f6445f commit f3a3751

File tree

7 files changed

+211
-112
lines changed

7 files changed

+211
-112
lines changed
Lines changed: 22 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -1,111 +1,29 @@
1-
import BarChart from 'components/ui/Charts/BarChart'
1+
import BarChart, { BarChartProps } from 'components/ui/Charts/BarChart'
22
import { EventChartData, isUnixMicro, LogData, unixMicroToIsoTimestamp } from '.'
3-
import { useMemo } from 'react'
43

54
interface Props {
6-
data?: EventChartData[]
5+
data: EventChartData[]
76
onBarClick: (isoTimestamp: string) => void
87
}
98

10-
const LogEventChart: React.FC<Props> = ({ data, onBarClick }) => {
11-
// TODO: remove once endpoint returns iso timestamp directly
12-
const transformedData = useMemo(() => {
13-
return data?.map((d) => {
14-
const iso = isUnixMicro(d.timestamp) ? unixMicroToIsoTimestamp(d.timestamp) : d.timestamp
15-
16-
return {
17-
...d,
18-
timestamp: iso,
19-
}
20-
})
21-
}, [JSON.stringify(data)])
22-
23-
if (!transformedData) return null
24-
25-
return (
26-
<BarChart
27-
minimalHeader
28-
size="tiny"
29-
yAxisKey="count"
30-
xAxisKey="timestamp"
31-
data={transformedData}
32-
title="Logs / Time"
33-
onBarClick={(v?: { activePayload?: { payload: any }[] }) => {
34-
if (!v || !v?.activePayload?.[0]?.payload) return
35-
const unixOrIsoTimestamp = v.activePayload[0].payload.timestamp
36-
const isoTimestamp = isUnixMicro(unixOrIsoTimestamp)
37-
? unixMicroToIsoTimestamp(unixOrIsoTimestamp)
38-
: unixOrIsoTimestamp
39-
// 60s before
40-
onBarClick(isoTimestamp)
41-
}}
42-
customDateFormat="MMM D, HH:mm:s"
43-
/>
44-
)
45-
}
46-
47-
// const useAggregated = (data: LogData[]) => {
48-
// const truncateToMinute = (micro: number) => Math.floor(micro / 1000 / 1000 / 60) * 60
49-
// const truncateToHour = (micro: number) => Math.floor(micro / 1000 / 1000 / 60 / 60) * 60 * 60
50-
// const getDiffMinute = (currentTimestamp: number, olderTimestamp: number) =>
51-
// Math.round((currentTimestamp - olderTimestamp) / 1000 / 60)
52-
// const getDiffHour = (currentTimestamp: number, olderTimestamp: number) =>
53-
// Math.round((currentTimestamp - olderTimestamp) / 1000 / 60 / 60)
54-
// const diffMultiplierMinute = (v: number) => v * 60
55-
// const diffMultiplierHour = (v: number) => v * 60 * 60
56-
// return useMemo(() => {
57-
// const oldest = data[data.length - 1]
58-
// if (!oldest) return
59-
// const latest = data[0]
60-
// const oldestDayjs = dayjs(oldest.timestamp / 1000)
61-
// const latestDayjs = dayjs(latest.timestamp / 1000)
62-
// let truncFunc = truncateToMinute
63-
// let getDiff = getDiffMinute
64-
// let diffMultiplier = diffMultiplierMinute
65-
// if (Math.abs(oldestDayjs.diff(latestDayjs, 'day', true)) > 0.25) {
66-
// truncFunc = truncateToHour
67-
// getDiff = getDiffHour
68-
// diffMultiplier = diffMultiplierHour
69-
// }
70-
71-
// const countMap = data
72-
// .map((d) => {
73-
// // truncate to per-minute
74-
// return { ...d, timestamp: truncateToMinute(d.timestamp) }
75-
// })
76-
// .reduce((acc, d) => {
77-
// if (acc[d.timestamp]) {
78-
// acc[d.timestamp] = acc[d.timestamp] + 1
79-
// } else {
80-
// acc[d.timestamp] = 1
81-
// }
82-
// return acc
83-
// }, {} as TimestampMap)
84-
85-
// // Add in additional data points for empty minutes
86-
// const oldestEvent = data[data.length - 1]
87-
// if (!oldestEvent) return []
88-
// const currentTimestamp = new Date().getTime()
89-
// const oldestTimestampMicro = oldestEvent.timestamp
90-
// const latestTimestamp = truncFunc(data[0]['timestamp'])
91-
// const diff = getDiff(currentTimestamp, oldestTimestampMicro / 1000)
92-
// for (const toAdd of Array.from(Array(diff).keys())) {
93-
// const tsToCheck = truncFunc(oldestTimestampMicro) + diffMultiplier(toAdd)
94-
// if (!(tsToCheck in countMap) && tsToCheck <= latestTimestamp) {
95-
// countMap[tsToCheck] = 0
96-
// }
97-
// }
98-
99-
// let aggregated = []
100-
// for (const [key, value] of Object.entries(countMap)) {
101-
// const v: number = Number(key)
102-
// aggregated.push({
103-
// timestamp: key,
104-
// timestampMicro: v * 1000 * 1000,
105-
// count: value,
106-
// })
107-
// }
108-
// return aggregated.sort((a, b) => a.timestampMicro - b.timestampMicro)
109-
// }, [JSON.stringify(data)])
110-
// }
9+
const LogEventChart: React.FC<Props> = ({ data, onBarClick }) => (
10+
<BarChart
11+
minimalHeader
12+
size="tiny"
13+
yAxisKey="count"
14+
xAxisKey="timestamp"
15+
data={data as unknown as BarChartProps['data']}
16+
title="Logs / Time"
17+
onBarClick={(v?: { activePayload?: { payload: any }[] }) => {
18+
if (!v || !v?.activePayload?.[0]?.payload) return
19+
const unixOrIsoTimestamp = v.activePayload[0].payload.timestamp
20+
const isoTimestamp = isUnixMicro(unixOrIsoTimestamp)
21+
? unixMicroToIsoTimestamp(unixOrIsoTimestamp)
22+
: unixOrIsoTimestamp
23+
// 60s before
24+
onBarClick(isoTimestamp)
25+
}}
26+
customDateFormat="MMM D, HH:mm:s"
27+
/>
28+
)
11129
export default LogEventChart

studio/components/interfaces/Settings/Logs/Logs.utils.ts

Lines changed: 75 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
import { Filters, LogData, LogsEndpointParams, LogsTableName, SQL_FILTER_TEMPLATES } from '.'
22
import dayjs, { Dayjs } from 'dayjs'
3-
import { get } from 'lodash'
3+
import { get, isEqual } from 'lodash'
44
import { StripeSubscription } from 'components/interfaces/Billing'
55
import { useMonaco } from '@monaco-editor/react'
66
import logConstants from 'shared-data/logConstants'
77
import BackwardIterator from 'components/ui/CodeEditor/Providers/BackwardIterator'
8-
import { uniqBy } from 'lodash'
8+
import uniqBy from 'lodash/uniqBy'
99
import { useEffect } from 'react'
10+
import utc from 'dayjs/plugin/utc'
11+
dayjs.extend(utc)
1012

1113
/**
1214
* Convert a micro timestamp from number/string to iso timestamp
@@ -228,13 +230,22 @@ export const genChartQuery = (
228230
) => {
229231
const [startOffset, trunc] = calcChartStart(params)
230232
const where = _genWhereStatement(table, filters)
233+
234+
let joins = 'cross join unnest(t.metadata) as metadata'
235+
if (table === LogsTableName.EDGE) {
236+
joins += ' \n cross join unnest(metadata.request) as request'
237+
joins += ' \n cross join unnest(metadata.response) as response'
238+
} else if (table === LogsTableName.POSTGRES) {
239+
joins += ' \n cross join unnest(metadata.parsed) as parsed'
240+
}
241+
231242
return `
232243
SELECT
233244
timestamp_trunc(t.timestamp, ${trunc}) as timestamp,
234245
count(t.timestamp) as count
235246
FROM
236247
${table} t
237-
cross join unnest(t.metadata) as metadata
248+
${joins}
238249
${
239250
where
240251
? where + ` and t.timestamp > '${startOffset.toISOString()}'`
@@ -349,3 +360,64 @@ export const useEditorHints = () => {
349360
}
350361
}, [monaco])
351362
}
363+
364+
/**
365+
* Assumes that all timestamps are in ISO-8601 UTC timezone.
366+
*
367+
* min/max are the datetime strings that extend beyond the given timeseries data.
368+
*/
369+
export const fillTimeseries = (
370+
timeseriesData: any[],
371+
timestampKey: string,
372+
valueKey: string,
373+
defaultValue: number,
374+
min?: string,
375+
max?: string
376+
) => {
377+
if (timeseriesData.length <= 1 && !(min || max)) return timeseriesData
378+
const dates: unknown[] = timeseriesData.map((datum) => dayjs.utc(datum[timestampKey]))
379+
380+
const maxDate = max ? dayjs.utc(max) : dayjs.utc(Math.max.apply(null, dates as number[]))
381+
const minDate = min ? dayjs.utc(min) : dayjs.utc(Math.min.apply(null, dates as number[]))
382+
383+
const truncationSample = timeseriesData.length > 0 ? timeseriesData[0][timestampKey] : min || max
384+
const truncation = getTimestampTruncation(truncationSample)
385+
386+
let newData = timeseriesData.map((datum) => {
387+
const iso = dayjs.utc(datum[timestampKey]).toISOString()
388+
datum[timestampKey] = iso
389+
return datum
390+
})
391+
392+
const diff = maxDate.diff(minDate, truncation as dayjs.UnitType)
393+
for (let i = 0; i <= diff; i++) {
394+
const dateToMaybeAdd = minDate.add(i, truncation as dayjs.ManipulateType)
395+
if (!dates.find((d) => isEqual(d, dateToMaybeAdd))) {
396+
newData.push({
397+
[timestampKey]: dateToMaybeAdd.toISOString(),
398+
[valueKey]: defaultValue,
399+
})
400+
}
401+
}
402+
403+
return newData
404+
}
405+
406+
export const getTimestampTruncation = (datetime: string): 'second' | 'minute' | 'hour' | 'day' => {
407+
const values = ['second', 'minute', 'hour', 'day'].map((key) =>
408+
dayjs(datetime).get(key as dayjs.UnitType)
409+
)
410+
const zeroCount = values.reduce((acc, value) => {
411+
if (value === 0) {
412+
acc += 1
413+
}
414+
return acc
415+
}, 0)
416+
let truncation = {
417+
0: 'second' as const,
418+
1: 'minute' as const,
419+
2: 'hour' as const,
420+
3: 'day' as const,
421+
}[zeroCount]!
422+
return truncation
423+
}

studio/components/interfaces/Settings/Logs/LogsPreviewer.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -191,9 +191,9 @@ export const LogsPreviewer: React.FC<Props> = ({
191191
}
192192
>
193193
<div className={condensedLayout ? 'px-4' : ''}>
194-
{showChart && (
194+
{!isLoading && showChart && (
195195
<LogEventChart
196-
data={!isLoading && eventChartData ? eventChartData : undefined}
196+
data={ eventChartData }
197197
onBarClick={(isoTimestamp) => {
198198
handleSearch('event-chart-bar-click', {
199199
query: filters.search_query as string,
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { fillTimeseries } from 'components/interfaces/Settings/Logs'
2+
import { useMemo } from 'react'
3+
4+
/**
5+
* Convenience hook for memoized filling of timeseries data.
6+
*/
7+
const useFillTimeseriesSorted = (...args: Parameters<typeof fillTimeseries>) => {
8+
return useMemo(() => {
9+
const filled = fillTimeseries(...args)
10+
return filled.sort((a, b) => {
11+
return (new Date(a[args[1]]) as any) - (new Date(b[args[1]]) as any)
12+
})
13+
}, [JSON.stringify(args[0])])
14+
}
15+
export default useFillTimeseriesSorted

studio/hooks/analytics/useLogsPreview.tsx

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,12 @@ import {
1515
LogsTableName,
1616
PREVIEWER_DATEPICKER_HELPERS,
1717
} from 'components/interfaces/Settings/Logs'
18-
import { Dispatch, SetStateAction, useEffect, useRef, useState } from 'react'
18+
import { Dispatch, SetStateAction, useEffect, useMemo, useRef, useState } from 'react'
1919
import { API_URL } from 'lib/constants'
2020
import { get } from 'lib/common/fetch'
2121
import dayjs from 'dayjs'
22+
import useFillTimeseriesSorted from './useFillTimeseriesSorted'
23+
import useTimeseriesUnixToIso from './useTimeseriesUnixToIso'
2224

2325
interface Data {
2426
logData: LogData[]
@@ -29,7 +31,7 @@ interface Data {
2931
filters: Filters
3032
params: LogsEndpointParams
3133
oldestTimestamp?: string
32-
eventChartData: EventChartData[] | null
34+
eventChartData: EventChartData[]
3335
}
3436
interface Handlers {
3537
loadOlder: () => void
@@ -176,6 +178,21 @@ function useLogsPreview(
176178
setFilters({ ...newFilters, ...filterOverride })
177179
}
178180
}
181+
182+
const normalizedEventChartData = useTimeseriesUnixToIso(
183+
eventChartResponse?.result || [],
184+
'timestamp'
185+
)
186+
187+
const eventChartData = useFillTimeseriesSorted(
188+
normalizedEventChartData,
189+
'timestamp',
190+
'count',
191+
0,
192+
params.iso_timestamp_start,
193+
params.iso_timestamp_end
194+
)
195+
179196
return [
180197
{
181198
newCount,
@@ -186,7 +203,7 @@ function useLogsPreview(
186203
filters,
187204
params,
188205
oldestTimestamp: oldestTimestamp ? String(oldestTimestamp) : undefined,
189-
eventChartData: eventChartResponse?.result || null,
206+
eventChartData,
190207
},
191208
{
192209
setFilters: handleSetFilters,
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { isUnixMicro, unixMicroToIsoTimestamp } from 'components/interfaces/Settings/Logs'
2+
import { useMemo } from 'react'
3+
4+
/**
5+
* Convenience hook for converting timeseries timestamp from unix microsecond to iso
6+
*
7+
* memoized
8+
*/
9+
const useTimeseriesUnixToIso = (data: any[], timestampKey: string) => {
10+
return useMemo(() => {
11+
// check if need to convert or not
12+
if (data.length === 0) return data
13+
if (!isUnixMicro(data[0][timestampKey])) return data
14+
15+
return data?.map((d) => {
16+
d[timestampKey] = unixMicroToIsoTimestamp(d[timestampKey])
17+
return d
18+
})
19+
}, [JSON.stringify(data)])
20+
}
21+
export default useTimeseriesUnixToIso

0 commit comments

Comments
 (0)