index.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421
  1. import {useState} from 'react';
  2. import {useTheme} from '@emotion/react';
  3. import styled from '@emotion/styled';
  4. import keyBy from 'lodash/keyBy';
  5. import moment from 'moment';
  6. import Badge from 'sentry/components/badge';
  7. import {Button} from 'sentry/components/button';
  8. import ButtonBar from 'sentry/components/buttonBar';
  9. import {IconChevron} from 'sentry/icons';
  10. import {t} from 'sentry/locale';
  11. import {space} from 'sentry/styles/space';
  12. import {Series} from 'sentry/types/echarts';
  13. import usePageFilters from 'sentry/utils/usePageFilters';
  14. import Chart from 'sentry/views/starfish/components/chart';
  15. import Detail from 'sentry/views/starfish/components/detailPanel';
  16. import ProfileView from 'sentry/views/starfish/modules/databaseModule/panel/profileView';
  17. import QueryTransactionTable, {
  18. PanelSort,
  19. } from 'sentry/views/starfish/modules/databaseModule/panel/queryTransactionTable';
  20. import SimilarQueryView from 'sentry/views/starfish/modules/databaseModule/panel/similarQueryView';
  21. import {
  22. useQueryPanelEventCount,
  23. useQueryPanelGraph,
  24. useQueryPanelTable,
  25. useQueryTransactionByTPMAndP75,
  26. } from 'sentry/views/starfish/modules/databaseModule/queries';
  27. import {queryToSeries} from 'sentry/views/starfish/modules/databaseModule/utils';
  28. import {getDateFilters} from 'sentry/views/starfish/utils/dates';
  29. import {zeroFillSeries} from 'sentry/views/starfish/utils/zeroFillSeries';
  30. import {DataRow, MainTableSort} from '../databaseTableView';
  31. const INTERVAL = 12;
  32. type DbQueryDetailProps = {
  33. isDataLoading: boolean;
  34. mainTableSort: MainTableSort;
  35. onRowChange: (row: DataRow | undefined) => void;
  36. row: DataRow;
  37. nextRow?: DataRow;
  38. prevRow?: DataRow;
  39. };
  40. export type TransactionListDataRow = {
  41. count: number;
  42. frequency: number;
  43. group_id: string;
  44. p75: number;
  45. transaction: string;
  46. uniqueEvents: number;
  47. };
  48. export default function QueryDetail({
  49. row,
  50. nextRow,
  51. prevRow,
  52. isDataLoading,
  53. onClose,
  54. onRowChange,
  55. mainTableSort,
  56. }: Partial<DbQueryDetailProps> & {
  57. isDataLoading: boolean;
  58. mainTableSort: MainTableSort;
  59. onClose: () => void;
  60. onRowChange: (row: DataRow) => void;
  61. }) {
  62. return (
  63. <Detail detailKey={row?.description} onClose={onClose}>
  64. {row && (
  65. <QueryDetailBody
  66. mainTableSort={mainTableSort}
  67. onRowChange={onRowChange}
  68. isDataLoading={isDataLoading}
  69. row={row}
  70. nextRow={nextRow}
  71. prevRow={prevRow}
  72. />
  73. )}
  74. </Detail>
  75. );
  76. }
  77. function QueryDetailBody({
  78. row,
  79. nextRow,
  80. prevRow,
  81. onRowChange,
  82. isDataLoading: isRowLoading,
  83. }: DbQueryDetailProps) {
  84. const theme = useTheme();
  85. const pageFilter = usePageFilters();
  86. const {startTime, endTime} = getDateFilters(pageFilter);
  87. const [sort, setSort] = useState<PanelSort>({
  88. direction: undefined,
  89. sortHeader: undefined,
  90. });
  91. const {isLoading, data: graphData} = useQueryPanelGraph(row, INTERVAL);
  92. const {isLoading: isTableLoading, data: tableData} = useQueryPanelTable(
  93. row,
  94. sort.sortHeader?.key,
  95. sort.direction
  96. );
  97. const {isLoading: isP75GraphLoading, data: transactionGraphData} =
  98. useQueryTransactionByTPMAndP75(tableData.map(d => d.transaction).splice(0, 5));
  99. const {isLoading: isEventCountLoading, data: eventCountData} =
  100. useQueryPanelEventCount(row);
  101. const isDataLoading =
  102. isLoading ||
  103. isTableLoading ||
  104. isEventCountLoading ||
  105. isRowLoading ||
  106. isP75GraphLoading;
  107. const eventCountMap = keyBy(eventCountData, 'transaction');
  108. const mergedTableData: TransactionListDataRow[] = tableData.map(data => {
  109. const {transaction} = data;
  110. const eventData = eventCountMap[transaction];
  111. if (eventData?.uniqueEvents) {
  112. const frequency = data.count / eventData.uniqueEvents;
  113. return {...data, frequency, ...eventData} as TransactionListDataRow;
  114. }
  115. return data as TransactionListDataRow;
  116. });
  117. const [countSeries, p75Series] = throughputQueryToChartData(
  118. graphData,
  119. startTime,
  120. endTime
  121. );
  122. const tpmTransactionSeries = queryToSeries(
  123. transactionGraphData,
  124. 'group',
  125. 'count()',
  126. startTime,
  127. endTime
  128. );
  129. const p75TransactionSeries = queryToSeries(
  130. transactionGraphData,
  131. 'group',
  132. 'p75(transaction.duration)',
  133. startTime,
  134. endTime
  135. );
  136. return (
  137. <div>
  138. <FlexRowContainer>
  139. <FlexRowItem>
  140. <h2>{t('Query Detail')}</h2>
  141. </FlexRowItem>
  142. <FlexRowItem>
  143. <SimplePagination
  144. disableLeft={!prevRow}
  145. disableRight={!nextRow}
  146. onLeftClick={() => onRowChange(prevRow)}
  147. onRightClick={() => onRowChange(nextRow)}
  148. />
  149. </FlexRowItem>
  150. </FlexRowContainer>
  151. <FlexRowContainer>
  152. <FlexRowItem>
  153. <SubHeader>
  154. {t('First Seen')}
  155. {row.newish === 1 && <Badge type="new" text="new" />}
  156. </SubHeader>
  157. <SubSubHeader>{row.firstSeen}</SubSubHeader>
  158. </FlexRowItem>
  159. <FlexRowItem>
  160. <SubHeader>
  161. {t('Last Seen')}
  162. {row.retired === 1 && <Badge type="warning" text="old" />}
  163. </SubHeader>
  164. <SubSubHeader>{row.lastSeen}</SubSubHeader>
  165. </FlexRowItem>
  166. <FlexRowItem>
  167. <SubHeader>{t('Total Time')}</SubHeader>
  168. <SubSubHeader>{row.total_time.toFixed(2)}ms</SubSubHeader>
  169. </FlexRowItem>
  170. </FlexRowContainer>
  171. <SubHeader>{t('Query Description')}</SubHeader>
  172. <FormattedCode>{highlightSql(row.formatted_desc, row)}</FormattedCode>
  173. <FlexRowContainer>
  174. <FlexRowItem>
  175. <SubHeader>{t('Throughput')}</SubHeader>
  176. <SubSubHeader>{row.epm.toFixed(3)}</SubSubHeader>
  177. <Chart
  178. statsPeriod="24h"
  179. height={140}
  180. data={[countSeries]}
  181. start=""
  182. end=""
  183. loading={isDataLoading}
  184. utc={false}
  185. stacked
  186. isLineChart
  187. disableXAxis
  188. hideYAxisSplitLine
  189. />
  190. </FlexRowItem>
  191. <FlexRowItem>
  192. <SubHeader>{t('Duration (P75)')}</SubHeader>
  193. <SubSubHeader>{row.p75.toFixed(3)}ms</SubSubHeader>
  194. <Chart
  195. statsPeriod="24h"
  196. height={140}
  197. data={[p75Series]}
  198. start=""
  199. end=""
  200. loading={isDataLoading}
  201. utc={false}
  202. chartColors={[theme.charts.getColorPalette(4)[3]]}
  203. stacked
  204. isLineChart
  205. disableXAxis
  206. hideYAxisSplitLine
  207. />
  208. </FlexRowItem>
  209. </FlexRowContainer>
  210. <FlexRowContainer>
  211. <FlexRowItem>
  212. <SubHeader>{t('Top 5 Transactions by Throughput')}</SubHeader>
  213. <Chart
  214. statsPeriod="24h"
  215. height={140}
  216. data={tpmTransactionSeries}
  217. start=""
  218. end=""
  219. loading={isDataLoading}
  220. grid={{
  221. left: '0',
  222. right: '0',
  223. top: '16px',
  224. bottom: '8px',
  225. }}
  226. utc={false}
  227. disableXAxis
  228. isLineChart
  229. hideYAxisSplitLine
  230. />
  231. </FlexRowItem>
  232. <FlexRowItem>
  233. <SubHeader>{t('Top 5 Transactions by P75')}</SubHeader>
  234. <Chart
  235. statsPeriod="24h"
  236. height={140}
  237. data={p75TransactionSeries}
  238. start=""
  239. end=""
  240. loading={isP75GraphLoading}
  241. grid={{
  242. left: '0',
  243. right: '0',
  244. top: '16px',
  245. bottom: '8px',
  246. }}
  247. utc={false}
  248. disableXAxis
  249. isLineChart
  250. hideYAxisSplitLine
  251. />
  252. </FlexRowItem>
  253. </FlexRowContainer>
  254. <QueryTransactionTable
  255. isDataLoading={isDataLoading}
  256. onClickSort={s => setSort(s)}
  257. row={row}
  258. sort={sort}
  259. tableData={mergedTableData}
  260. />
  261. <FlexRowContainer>
  262. <FlexRowItem>
  263. <SubHeader>{t('Example Profile')}</SubHeader>
  264. <ProfileView
  265. spanHash={row.group_id}
  266. transactionNames={tableData.map(d => d.transaction)}
  267. />
  268. </FlexRowItem>
  269. </FlexRowContainer>
  270. <FlexRowContainer>
  271. <FlexRowItem>
  272. <SubHeader>{t('Similar Queries')}</SubHeader>
  273. <SimilarQueryView mainTableRow={row} />
  274. </FlexRowItem>
  275. </FlexRowContainer>
  276. </div>
  277. );
  278. }
  279. type SimplePaginationProps = {
  280. disableLeft?: boolean;
  281. disableRight?: boolean;
  282. onLeftClick?: () => void;
  283. onRightClick?: () => void;
  284. };
  285. function SimplePagination(props: SimplePaginationProps) {
  286. return (
  287. <ButtonBar merged>
  288. <Button
  289. icon={<IconChevron direction="left" size="sm" />}
  290. aria-label={t('Previous')}
  291. disabled={props.disableLeft}
  292. onClick={props.onLeftClick}
  293. />
  294. <Button
  295. icon={<IconChevron direction="right" size="sm" />}
  296. aria-label={t('Next')}
  297. onClick={props.onRightClick}
  298. disabled={props.disableRight}
  299. />
  300. </ButtonBar>
  301. );
  302. }
  303. export const highlightSql = (description: string, queryDetail: DataRow) => {
  304. let acc = '';
  305. return description.split('').map((token, i) => {
  306. acc += token;
  307. let final: string | React.ReactElement | null = null;
  308. if (acc === queryDetail.action) {
  309. final = <Operation key={i}>{queryDetail.action} </Operation>;
  310. } else if (acc === queryDetail.domain) {
  311. final = <Domain key={i}>{queryDetail.domain} </Domain>;
  312. } else if (
  313. ['FROM', 'INNER', 'JOIN', 'WHERE', 'ON', 'AND', 'NOT', 'NULL', 'IS'].includes(acc)
  314. ) {
  315. final = <Keyword key={i}>{acc}</Keyword>;
  316. } else if (['(', ')'].includes(acc)) {
  317. final = <Bracket key={i}>{acc}</Bracket>;
  318. } else if (token === ' ' || token === '\n' || description[i + 1] === ')') {
  319. final = acc;
  320. } else if (i === description.length - 1) {
  321. final = acc;
  322. }
  323. if (final) {
  324. acc = '';
  325. const result = final;
  326. final = null;
  327. return result;
  328. }
  329. return null;
  330. });
  331. };
  332. const throughputQueryToChartData = (
  333. data: any,
  334. startTime: moment.Moment,
  335. endTime: moment.Moment
  336. ): Series[] => {
  337. const countSeries: Series = {seriesName: 'count()', data: [] as any[]};
  338. const p75Series: Series = {seriesName: 'p75()', data: [] as any[]};
  339. data.forEach(({count, p75, interval}: any) => {
  340. countSeries.data.push({value: count, name: interval});
  341. p75Series.data.push({value: p75, name: interval});
  342. });
  343. return [
  344. zeroFillSeries(countSeries, moment.duration(INTERVAL, 'hours'), startTime, endTime),
  345. zeroFillSeries(p75Series, moment.duration(INTERVAL, 'hours'), startTime, endTime),
  346. ];
  347. };
  348. const SubHeader = styled('h3')`
  349. color: ${p => p.theme.gray300};
  350. font-size: ${p => p.theme.fontSizeLarge};
  351. `;
  352. const SubSubHeader = styled('h4')`
  353. margin: 0;
  354. font-weight: normal;
  355. `;
  356. const FlexRowContainer = styled('div')`
  357. display: flex;
  358. & > div:last-child {
  359. padding-right: ${space(1)};
  360. }
  361. padding-bottom: ${space(2)};
  362. `;
  363. const FlexRowItem = styled('div')`
  364. padding-right: ${space(4)};
  365. flex: 1;
  366. `;
  367. const FormattedCode = styled('div')`
  368. padding: ${space(1)};
  369. margin-bottom: ${space(3)};
  370. background: ${p => p.theme.backgroundSecondary};
  371. border-radius: ${p => p.theme.borderRadius};
  372. overflow-x: auto;
  373. white-space: pre;
  374. `;
  375. const Operation = styled('b')`
  376. color: ${p => p.theme.blue400};
  377. `;
  378. const Domain = styled('b')`
  379. color: ${p => p.theme.green400};
  380. margin-right: -${space(0.5)};
  381. `;
  382. const Keyword = styled('b')`
  383. color: ${p => p.theme.yellow400};
  384. `;
  385. const Bracket = styled('b')`
  386. color: ${p => p.theme.pink400};
  387. `;