logsTable.tsx 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272
  1. import {Fragment, useCallback, useState} from 'react';
  2. import {useTheme} from '@emotion/react';
  3. import EmptyStateWarning, {EmptyStreamWrapper} from 'sentry/components/emptyStateWarning';
  4. import ExternalLink from 'sentry/components/links/externalLink';
  5. import LoadingIndicator from 'sentry/components/loadingIndicator';
  6. import Pagination from 'sentry/components/pagination';
  7. import {LOGS_PROPS_DOCS_URL} from 'sentry/constants';
  8. import {IconArrow, IconWarning} from 'sentry/icons';
  9. import {IconChevron} from 'sentry/icons/iconChevron';
  10. import {t, tct} from 'sentry/locale';
  11. import {defined} from 'sentry/utils';
  12. import {
  13. useLogsCursor,
  14. useLogsSearch,
  15. useLogsSortBys,
  16. useSetLogsCursor,
  17. useSetLogsSortBys,
  18. } from 'sentry/views/explore/contexts/logs/logsPageParams';
  19. import {
  20. bodyRenderer,
  21. HiddenLogAttributes,
  22. LogAttributesRendererMap,
  23. severityTextRenderer,
  24. TimestampRenderer,
  25. } from 'sentry/views/explore/logs/fieldRenderers';
  26. import {LogFieldsTree} from 'sentry/views/explore/logs/logFieldsTree';
  27. import {
  28. type OurLogFieldKey,
  29. OurLogKnownFieldKey,
  30. type OurLogsResponseItem,
  31. } from 'sentry/views/explore/logs/types';
  32. import {
  33. useExploreLogsTable,
  34. useExploreLogsTableRow,
  35. } from 'sentry/views/explore/logs/useLogsQuery';
  36. import {EmptyStateText} from 'sentry/views/traces/styles';
  37. import {
  38. DetailsFooter,
  39. DetailsGrid,
  40. DetailsWrapper,
  41. getLogColors,
  42. HeaderCell,
  43. LogDetailsTitle,
  44. LogPanelContent,
  45. StyledChevronButton,
  46. StyledPanel,
  47. StyledPanelItem,
  48. } from './styles';
  49. import {getLogBodySearchTerms, getLogSeverityLevel} from './utils';
  50. type LogsRowProps = {
  51. dataRow: OurLogsResponseItem;
  52. highlightTerms: string[];
  53. };
  54. export function LogsTable() {
  55. const search = useLogsSearch();
  56. const cursor = useLogsCursor();
  57. const setCursor = useSetLogsCursor();
  58. const {data, isError, isPending, pageLinks} = useExploreLogsTable({
  59. limit: 100,
  60. search,
  61. cursor,
  62. });
  63. const isEmpty = !isPending && !isError && (data?.length ?? 0) === 0;
  64. const highlightTerms = getLogBodySearchTerms(search);
  65. const sortBys = useLogsSortBys();
  66. const setSortBys = useSetLogsSortBys();
  67. const headers: Array<{align: 'left' | 'right'; field: OurLogFieldKey; label: string}> =
  68. [
  69. {field: OurLogKnownFieldKey.SEVERITY_NUMBER, label: t('Severity'), align: 'left'},
  70. {field: OurLogKnownFieldKey.BODY, label: t('Message'), align: 'left'},
  71. {field: OurLogKnownFieldKey.TIMESTAMP, label: t('Timestamp'), align: 'right'},
  72. ];
  73. return (
  74. <Fragment>
  75. <StyledPanel>
  76. <LogPanelContent>
  77. {headers.map((header, index) => {
  78. const direction = sortBys.find(s => s.field === header.field)?.kind;
  79. return (
  80. <HeaderCell
  81. key={index}
  82. align={header.align}
  83. lightText
  84. onClick={() => setSortBys([{field: header.field}])}
  85. >
  86. {header.label}
  87. {defined(direction) && (
  88. <IconArrow
  89. size="xs"
  90. direction={
  91. direction === 'desc'
  92. ? 'down'
  93. : direction === 'asc'
  94. ? 'up'
  95. : undefined
  96. }
  97. />
  98. )}
  99. </HeaderCell>
  100. );
  101. })}
  102. {isPending && (
  103. <StyledPanelItem span={3} overflow>
  104. <LoadingIndicator />
  105. </StyledPanelItem>
  106. )}
  107. {isError && (
  108. <StyledPanelItem span={3} overflow>
  109. <EmptyStreamWrapper>
  110. <IconWarning color="gray300" size="lg" />
  111. </EmptyStreamWrapper>
  112. </StyledPanelItem>
  113. )}
  114. {isEmpty && (
  115. <StyledPanelItem span={3} overflow>
  116. <EmptyStateWarning withIcon>
  117. <EmptyStateText size="fontSizeExtraLarge">
  118. {t('No logs found')}
  119. </EmptyStateText>
  120. <EmptyStateText size="fontSizeMedium">
  121. {tct('Try adjusting your filters or refer to [docSearchProps].', {
  122. docSearchProps: (
  123. <ExternalLink href={LOGS_PROPS_DOCS_URL}>
  124. {t('docs for search properties')}
  125. </ExternalLink>
  126. ),
  127. })}
  128. </EmptyStateText>
  129. </EmptyStateWarning>
  130. </StyledPanelItem>
  131. )}
  132. {data?.map((row, index) => (
  133. <LogsRow key={index} dataRow={row} highlightTerms={highlightTerms} />
  134. ))}
  135. </LogPanelContent>
  136. </StyledPanel>
  137. <Pagination pageLinks={pageLinks} onCursor={setCursor} />
  138. </Fragment>
  139. );
  140. }
  141. function LogsRow({dataRow, highlightTerms}: LogsRowProps) {
  142. const [expanded, setExpanded] = useState<boolean>(false);
  143. const onClickExpand = useCallback(() => setExpanded(e => !e), [setExpanded]);
  144. const theme = useTheme();
  145. const severityNumber = dataRow[OurLogKnownFieldKey.SEVERITY_NUMBER];
  146. const severityText = dataRow[OurLogKnownFieldKey.SEVERITY_TEXT];
  147. const level = getLogSeverityLevel(
  148. typeof severityNumber === 'number' ? severityNumber : null,
  149. typeof severityText === 'string' ? severityText : null
  150. );
  151. const logColors = getLogColors(level, theme);
  152. return (
  153. <Fragment>
  154. <StyledPanelItem align="left" center onClick={onClickExpand}>
  155. <StyledChevronButton
  156. icon={<IconChevron size="xs" direction={expanded ? 'down' : 'right'} />}
  157. aria-label={t('Toggle trace details')}
  158. aria-expanded={expanded}
  159. size="zero"
  160. borderless
  161. />
  162. {severityTextRenderer({
  163. attribute_value: severityText,
  164. tableResultLogRow: dataRow,
  165. extra: {
  166. highlightTerms,
  167. logColors,
  168. useFullSeverityText: false,
  169. renderSeverityCircle: true,
  170. },
  171. })}
  172. </StyledPanelItem>
  173. <StyledPanelItem overflow>
  174. {bodyRenderer({
  175. attribute_value: dataRow[OurLogKnownFieldKey.BODY],
  176. extra: {
  177. highlightTerms,
  178. logColors,
  179. wrapBody: false,
  180. },
  181. })}
  182. </StyledPanelItem>
  183. <StyledPanelItem align="right">
  184. <TimestampRenderer
  185. attribute_value={dataRow[OurLogKnownFieldKey.TIMESTAMP]}
  186. extra={{
  187. highlightTerms,
  188. logColors,
  189. }}
  190. />
  191. </StyledPanelItem>
  192. {expanded && <LogDetails dataRow={dataRow} highlightTerms={highlightTerms} />}
  193. </Fragment>
  194. );
  195. }
  196. function LogDetails({
  197. dataRow,
  198. highlightTerms,
  199. }: {
  200. dataRow: OurLogsResponseItem;
  201. highlightTerms: string[];
  202. }) {
  203. const severityNumber = dataRow[OurLogKnownFieldKey.SEVERITY_NUMBER];
  204. const severityText = dataRow[OurLogKnownFieldKey.SEVERITY_TEXT];
  205. const level = getLogSeverityLevel(
  206. typeof severityNumber === 'number' ? severityNumber : null,
  207. typeof severityText === 'string' ? severityText : null
  208. );
  209. const missingLogId = !dataRow[OurLogKnownFieldKey.ID];
  210. const {data, isPending} = useExploreLogsTableRow({
  211. log_id: String(dataRow[OurLogKnownFieldKey.ID] ?? ''),
  212. project_id: String(dataRow[OurLogKnownFieldKey.PROJECT_ID] ?? ''),
  213. enabled: !missingLogId,
  214. });
  215. const theme = useTheme();
  216. const logColors = getLogColors(level, theme);
  217. if (missingLogId) {
  218. return (
  219. <DetailsWrapper span={3}>
  220. <EmptyStreamWrapper>
  221. <IconWarning color="gray300" size="lg" />
  222. </EmptyStreamWrapper>
  223. </DetailsWrapper>
  224. );
  225. }
  226. return (
  227. <DetailsWrapper span={3}>
  228. {isPending && <LoadingIndicator />}
  229. {!isPending && data && (
  230. <Fragment>
  231. <DetailsGrid>
  232. <LogDetailsTitle>{t('Log')}</LogDetailsTitle>
  233. <LogFieldsTree
  234. attributes={data.attributes}
  235. hiddenAttributes={HiddenLogAttributes}
  236. renderers={LogAttributesRendererMap}
  237. renderExtra={{
  238. highlightTerms,
  239. logColors,
  240. }}
  241. />
  242. </DetailsGrid>
  243. <DetailsFooter logColors={logColors}>
  244. {bodyRenderer({
  245. attribute_value: dataRow[OurLogKnownFieldKey.BODY],
  246. extra: {
  247. highlightTerms,
  248. logColors,
  249. wrapBody: true,
  250. },
  251. })}
  252. </DetailsFooter>
  253. </Fragment>
  254. )}
  255. </DetailsWrapper>
  256. );
  257. }