hostTable.tsx 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342
  1. import {ReactNode} from 'react';
  2. import {Theme, useTheme} from '@emotion/react';
  3. import styled from '@emotion/styled';
  4. import {Location} from 'history';
  5. import groupBy from 'lodash/groupBy';
  6. import orderBy from 'lodash/orderBy';
  7. import moment from 'moment';
  8. import GridEditable, {
  9. COL_WIDTH_UNDEFINED,
  10. GridColumnHeader,
  11. } from 'sentry/components/gridEditable';
  12. import {space} from 'sentry/styles/space';
  13. import {Series} from 'sentry/types/echarts';
  14. import {getDuration} from 'sentry/utils/formatters';
  15. import usePageFilters from 'sentry/utils/usePageFilters';
  16. import Sparkline from 'sentry/views/starfish/components/sparkline';
  17. import {useSpansQuery} from 'sentry/views/starfish/utils/useSpansQuery';
  18. import {zeroFillSeries} from 'sentry/views/starfish/utils/zeroFillSeries';
  19. import {
  20. getEndpointDomainsEventView,
  21. getEndpointDomainsQuery,
  22. getHostListEventView,
  23. getHostListQuery,
  24. } from './queries';
  25. type Props = {
  26. location: Location;
  27. setDomainFilter: (domain: string) => void;
  28. };
  29. type HostTableRow = {
  30. duration: Series;
  31. failure_rate: Series;
  32. host: string;
  33. max: number;
  34. p50: number;
  35. p95: number;
  36. p99: number;
  37. };
  38. const COLUMN_ORDER = [
  39. {
  40. key: 'host',
  41. name: 'Host',
  42. width: COL_WIDTH_UNDEFINED,
  43. },
  44. {
  45. key: 'duration',
  46. name: 'Response Time',
  47. width: 220,
  48. },
  49. {
  50. key: 'failure_rate',
  51. name: 'Failure Rate',
  52. width: 220,
  53. },
  54. {
  55. key: 'p50',
  56. name: 'P50',
  57. width: 200,
  58. },
  59. {
  60. key: 'p95',
  61. name: 'P95',
  62. width: 200,
  63. },
  64. {
  65. key: 'total_exclusive_time',
  66. name: 'Total Exclusive Time',
  67. width: 200,
  68. },
  69. ];
  70. export default function HostTable({location, setDomainFilter}: Props) {
  71. const pageFilter = usePageFilters();
  72. const theme = useTheme();
  73. const queryString = getHostListQuery({
  74. datetime: pageFilter.selection.datetime,
  75. });
  76. const eventView = getHostListEventView({
  77. datetime: pageFilter.selection.datetime,
  78. });
  79. const aggregateQueryString = getEndpointDomainsQuery({
  80. datetime: pageFilter.selection.datetime,
  81. });
  82. const aggregateEventView = getEndpointDomainsEventView({
  83. datetime: pageFilter.selection.datetime,
  84. });
  85. const {isLoading: areHostsLoading, data: hostsData} = useSpansQuery({
  86. eventView,
  87. queryString,
  88. initialData: [],
  89. });
  90. const {isLoading: areHostAggregatesLoading, data: aggregateHostsData} = useSpansQuery({
  91. eventView: aggregateEventView,
  92. queryString: aggregateQueryString,
  93. initialData: [],
  94. });
  95. const dataByHost = groupBy(hostsData, 'domain');
  96. // Filter out localhost and any IP addresses (probably an internal service)
  97. const hosts = Object.keys(dataByHost).filter(host => !host.match(/\d\.\d|localhost/));
  98. const startDate = moment(orderBy(hostsData, 'interval', 'asc')[0]?.interval);
  99. const endDate = moment(orderBy(hostsData, 'interval', 'desc')[0]?.interval);
  100. let totalTotalExclusiveTime = 0;
  101. let totalP50 = 0;
  102. let totalP95 = 0;
  103. const tableData: HostTableRow[] = hosts
  104. .map(host => {
  105. const durationSeries: Series = zeroFillSeries(
  106. {
  107. seriesName: host,
  108. data: dataByHost[host].map(datum => ({
  109. name: datum.interval,
  110. value: datum.p99,
  111. })),
  112. },
  113. moment.duration(12, 'hours'),
  114. startDate,
  115. endDate
  116. );
  117. const failureRateSeries: Series = zeroFillSeries(
  118. {
  119. seriesName: host,
  120. data: dataByHost[host].map(datum => ({
  121. name: datum.interval,
  122. value: datum.failure_rate,
  123. })),
  124. },
  125. moment.duration(12, 'hours'),
  126. startDate,
  127. endDate
  128. );
  129. const {
  130. 'p50(span.self_time)': p50,
  131. 'p99(span.self_time)': p99,
  132. 'p95(span.self_time)': p95,
  133. 'p100(span.self_time)': max,
  134. 'sum(span.self_time)': total_exclusive_time,
  135. } = aggregateHostsData?.find(aggregate => aggregate.domain === host) ?? {};
  136. totalTotalExclusiveTime += total_exclusive_time;
  137. totalP50 += p50;
  138. totalP95 += p95;
  139. return {
  140. host,
  141. duration: durationSeries,
  142. failure_rate: failureRateSeries,
  143. p50,
  144. p99,
  145. p95,
  146. max,
  147. total_exclusive_time,
  148. };
  149. })
  150. .filter(row => {
  151. return row.duration.data.length > 0;
  152. })
  153. .sort((a, b) => b.total_exclusive_time - a.total_exclusive_time);
  154. return (
  155. <GridEditable
  156. isLoading={areHostsLoading || areHostAggregatesLoading}
  157. data={tableData}
  158. columnOrder={COLUMN_ORDER}
  159. columnSortBy={[]}
  160. grid={{
  161. renderHeadCell,
  162. renderBodyCell: (column: GridColumnHeader, row: HostTableRow) =>
  163. renderBodyCell({
  164. column,
  165. row,
  166. theme,
  167. totalTotalExclusiveTime,
  168. totalP50,
  169. totalP95,
  170. setDomainFilter,
  171. }),
  172. }}
  173. location={location}
  174. height={400}
  175. scrollable
  176. stickyHeader
  177. />
  178. );
  179. }
  180. function renderHeadCell(column: GridColumnHeader): React.ReactNode {
  181. return column.name;
  182. }
  183. function renderBodyCell({
  184. column,
  185. row,
  186. theme,
  187. totalTotalExclusiveTime,
  188. totalP50,
  189. totalP95,
  190. setDomainFilter,
  191. }: {
  192. column: GridColumnHeader;
  193. row: HostTableRow;
  194. setDomainFilter: (domain: string) => void;
  195. theme: Theme;
  196. totalP50: number;
  197. totalP95: number;
  198. totalTotalExclusiveTime: number;
  199. }): React.ReactNode {
  200. if (column.key === 'host') {
  201. return <a onClick={() => setDomainFilter(row.host)}>{row[column.key]}</a>;
  202. }
  203. if (column.key === 'duration') {
  204. const series: Series = row[column.key];
  205. if (series) {
  206. return <Sparkline color="rgb(242, 183, 18)" series={series} />;
  207. }
  208. return 'Loading';
  209. }
  210. if (column.key === 'failure_rate') {
  211. const series: Series = row[column.key];
  212. if (series) {
  213. return <Sparkline color="#ef7061" series={series} />;
  214. }
  215. return 'Loading';
  216. }
  217. if (column.key === 'total_exclusive_time') {
  218. return (
  219. <MeterBar
  220. minWidth={0.1}
  221. meterItems={['total_exclusive_time']}
  222. row={row}
  223. total={totalTotalExclusiveTime}
  224. color={theme.green300}
  225. />
  226. );
  227. }
  228. if (column.key === 'p50') {
  229. return (
  230. <MeterBar
  231. minWidth={0.1}
  232. meterItems={['p50']}
  233. row={row}
  234. total={totalP50}
  235. color={theme.blue300}
  236. />
  237. );
  238. }
  239. if (column.key === 'p95') {
  240. return (
  241. <MeterBar
  242. minWidth={0.1}
  243. meterItems={['p95']}
  244. row={row}
  245. total={totalP95}
  246. color={theme.red300}
  247. />
  248. );
  249. }
  250. return row[column.key];
  251. }
  252. export function MeterBar({
  253. minWidth,
  254. meterItems,
  255. row,
  256. total,
  257. color,
  258. meterText,
  259. }: {
  260. color: string;
  261. meterItems: string[];
  262. minWidth: number;
  263. row: any;
  264. total: number;
  265. meterText?: ReactNode;
  266. }) {
  267. const widths = [] as number[];
  268. meterItems.reduce((acc, item, index) => {
  269. const width = Math.max(
  270. Math.min(
  271. (100 * row[item]) / total - acc,
  272. 100 - acc - minWidth * (meterItems.length - index)
  273. ),
  274. minWidth
  275. );
  276. widths.push(width);
  277. return acc + width;
  278. }, 0);
  279. return (
  280. <span>
  281. <MeterText>
  282. {meterText ?? `${getDuration(row[meterItems[0]] / 1000, 0, true, true)}`}
  283. </MeterText>
  284. <MeterContainer width={100}>
  285. <Meter width={widths[0]} color={color} />
  286. </MeterContainer>
  287. </span>
  288. );
  289. }
  290. const MeterContainer = styled('span')<{width: number}>`
  291. display: flex;
  292. width: ${p => p.width}%;
  293. height: ${space(1)};
  294. background-color: ${p => p.theme.gray100};
  295. margin-bottom: 4px;
  296. `;
  297. const Meter = styled('span')<{
  298. color: string;
  299. width: number;
  300. }>`
  301. display: block;
  302. width: ${p => p.width}%;
  303. height: 100%;
  304. background-color: ${p => p.color};
  305. `;
  306. const MeterText = styled('span')`
  307. font-size: ${p => p.theme.fontSizeExtraSmall};
  308. color: ${p => p.theme.gray300};
  309. white-space: nowrap;
  310. `;