diff --git a/api/clickhouse-client.ts b/api/clickhouse-client.ts index 3eaf7c9..5ca089c 100644 --- a/api/clickhouse-client.ts +++ b/api/clickhouse-client.ts @@ -70,19 +70,11 @@ export const client: ReturnType = isLocal }) const numberToHex128 = (() => { - const alphabet = new TextEncoder().encode('0123456789abcdef') - const output = new Uint8Array(16) - const view = new DataView(new Uint8Array(8).buffer) - const dec = new TextDecoder() + const output = new Uint8Array(8) + const view = new DataView(output.buffer) return (id: number) => { view.setFloat64(0, id, false) - let i = -1 - while (++i < 8) { - const x = view.getUint8(i) - output[i * 2] = alphabet[x >> 4] - output[i * 2 + 1] = alphabet[x & 0xF] - } - return dec.decode(output) + return output.toHex() } })() diff --git a/web/pages/DeploymentPage.tsx b/web/pages/DeploymentPage.tsx index dccc66e..cb0bca8 100644 --- a/web/pages/DeploymentPage.tsx +++ b/web/pages/DeploymentPage.tsx @@ -95,6 +95,32 @@ const getColumnConfig = (col?: ColumnDef) => { COLUMN_STYLES.default } +const isTraceColumn = (col?: ColumnDef) => { + const name = col?.name.toLowerCase() || '' + return name === 'trace' || name === 'trace_id' +} + +const findTableWithTraceColumn = () => { + const currentTable = url.params.table || schema.data?.tables?.[0]?.table + const currentTableDef = schema.data?.tables?.find((t) => + t.table === currentTable + ) + const currentTraceCol = currentTableDef?.columns.find(isTraceColumn) + if (currentTable && currentTraceCol) { + return { table: currentTable, column: currentTraceCol.name } + } + + const tableWithTrace = schema.data?.tables?.find((t) => + t.columns.some(isTraceColumn) + ) + if (!tableWithTrace) return null + + return { + table: tableWithTrace.table, + column: tableWithTrace.columns.find(isTraceColumn)!.name, + } +} + const isDateColumn = (col?: ColumnDef, value?: unknown) => { const type = col?.type || '' const name = col?.name || '' @@ -165,27 +191,44 @@ const DataCell = ( const isEnum = col?.relation?.type === 'enum' const isTableRel = col?.relation?.type === 'table' const isPK = col?.isPrimaryKey + const isTraceCol = isTraceColumn(col) const rawValue = row[name] const value = isEnum ? (row[`inline_${name}`] ?? rawValue) : rawValue + const traceParams = isTraceCol && !!rawValue + ? { + tab: 'logs', + drawer: null, + 'row-id': null, + rt: null, + fl: `trace_id,eq,${numberToHex128(Number(rawValue))}`, + lq: null, + sl: null, + } + : null + return ( - {(isTableRel || isPK) && !!rawValue + {(isTableRel || isPK || traceParams) && !!rawValue ? ( e.stopPropagation()} > {isTableRel && ( )} + {isTraceCol && ( + + )} ) : } @@ -1051,19 +1094,11 @@ const onScrollLogs = (e: Event) => { } const parseHex128 = (() => { - const alphabet = new TextEncoder().encode('0123456789abcdef') - const alphabetMap = new Uint8Array(256) - const enc = new TextEncoder() - alphabet.forEach((c, i) => alphabetMap[c] = i) + const output = new Uint8Array(8) + const view = new DataView(output.buffer) return (encoded: string) => { - const bytes = enc.encode(encoded) - const buffer = new Uint8Array(8) - const view = new DataView(buffer.buffer) - let i = -1 - while (++i < 8) { - const hi = alphabetMap[bytes[i * 2]]! - const lo = alphabetMap[bytes[i * 2 + 1]]! - buffer[i] = (hi << 4) | lo + for (let i = 0; i < 8; i++) { + output[i] = parseInt(encoded.slice(i * 2, i * 2 + 2), 16) } const value = view.getFloat64(0, false) return { @@ -1074,6 +1109,15 @@ const parseHex128 = (() => { } })() +const numberToHex128 = (() => { + const output = new Uint8Array(8) + const view = new DataView(output.buffer) + return (id: number) => { + view.setFloat64(0, id, false) + return output.toHex() + } +})() + const severityConfig = { DEBUG: { icon: Bug, color: 'text-info', bg: 'bg-info/10' }, INFO: { icon: Info, color: 'text-info', bg: 'bg-info/10' }, @@ -1262,6 +1306,73 @@ const BodyBlock = ({ body }: { body: string }) => ( ) +const RelatedTraceButton = ( + { direction }: { direction: 'tables' | 'logs' }, +) => { + const isTables = direction === 'tables' + + const row = rowDetailsData.data?.rows?.[0] as AnyRecord | undefined + const tableName = url.params.rt || url.params.table || + schema.data?.tables?.[0]?.table + const tableDef = schema.data?.tables?.find((t) => t.table === tableName) + const traceCol = tableDef?.columns.find((c) => isTraceColumn(c)) + const logId = url.params['log-id'] + + if (isTables && !logId) return null + if (!isTables && !traceCol) return null + + const rawValue = isTables + ? logDetailsData.data?.[0].trace_id + : row?.[traceCol!.name] + + if (!rawValue) return null + const trace = isTables + ? parseHex128(String(rawValue)).value + : numberToHex128(Number(rawValue)) + + const target = isTables ? findTableWithTraceColumn() : null + if (isTables && !target) return null + + const params = isTables + ? { + tab: 'tables' as const, + table: target!.table, + drawer: null, + 'log-id': null, + ft: `${target!.column},eq,${trace}`, + qt: null, + st: null, + tpage: null, + } + : { + tab: 'logs' as const, + drawer: null, + 'row-id': null, + rt: null, + fl: `trace_id,eq,${trace}`, + lq: null, + sl: null, + } + + const label = isTables ? 'View related data' : 'View related logs' + const title = isTables + ? `View ${target!.table} rows for trace ${trace}` + : `View logs for trace ${trace}` + const Icon = isTables ? Database : FileText + + return ( + + + {label} + + ) +} + // Attributes block for JSON display const AttributesBlock = ( { attributes }: { attributes: Record }, @@ -2277,13 +2388,16 @@ const RowDetails = () => {

{url.params.rt ? `Related: ${url.params.rt}` : 'Row Details'}

- - - +
+ + + + +
@@ -2516,13 +2630,16 @@ const LogDetails = () => {

{log.event_name}

- - - +
+ + + + +