diff --git a/hack/docker-compose/docker-compose.test.yml b/hack/docker-compose/docker-compose.test.yml index a91092371..78994da62 100644 --- a/hack/docker-compose/docker-compose.test.yml +++ b/hack/docker-compose/docker-compose.test.yml @@ -28,11 +28,17 @@ services: - inner log-generator: - image: quay.io/rojacob/cluster-logging-load-client:0.0.1-db25b80 + image: mingrammer/flog:0.4.3 restart: unless-stopped volumes: - ./docker-compose-logs:/var/log - command: generate --output-format=json --destination=file --file=/var/log/generated.log --log-lines-rate=5000 --threads=3 + command: + - --loop + - --format=json + - --type=log + - --number=10 + - --delay=100ms + - --output=/var/log/fake.log networks: - inner diff --git a/hack/docker-compose/loki/promtail-config.yml b/hack/docker-compose/loki/promtail-config.yml index 585d71f81..eb8ec7eb8 100644 --- a/hack/docker-compose/loki/promtail-config.yml +++ b/hack/docker-compose/loki/promtail-config.yml @@ -11,8 +11,12 @@ clients: scrape_configs: - job_name: system pipeline_stages: - - regex: - expression: '.*"lvl":"(?P[a-zA-Z]+)"' + - json: + expressions: + status: status + - template: + source: level + template: '{{ if ge .status "500" }}critical{{ else if ge .status "400" }}error{{ else if ge .status "300" }}warning{{ else if ge .status "200" }}info{{ else }}debug{{ end }}' - labels: level: static_configs: @@ -29,13 +33,17 @@ scrape_configs: __path__: /var/log/*log - job_name: system2 pipeline_stages: - - regex: - expression: '.*"lvl":"(?P[a-zA-Z]+)"' + - json: + expressions: + status: status + - template: + source: level + template: '{{ if ge .status "500" }}critical{{ else if ge .status "400" }}error{{ else if ge .status "300" }}warning{{ else if ge .status "200" }}info{{ else }}debug{{ end }}' + - template: + source: severity_text + template: '{{ .level }}' - labels: level: - - regex: - expression: '.*"lvl":"(?P[a-zA-Z]+)"' - - labels: severity_text: static_configs: - targets: diff --git a/web/src/components/alerts/logs-alerts-metrics.tsx b/web/src/components/alerts/logs-alerts-metrics.tsx index 768bc8a0c..98cf3320b 100644 --- a/web/src/components/alerts/logs-alerts-metrics.tsx +++ b/web/src/components/alerts/logs-alerts-metrics.tsx @@ -16,16 +16,20 @@ const LOKI_TENANT_LABEL_KEY = 'tenantId'; const LogsAlertMetrics: React.FC = ({ rule }) => { const { t } = useTranslation('plugin__logging-view-plugin'); const { getLogs, logsData, logsError, isLoadingLogsData } = useLogs(); - const { config } = useLogsConfig(); + const { config, configLoaded } = useLogsConfig(); const tenant = rule?.labels?.[config.alertingRuleTenantLabelKey ?? LOKI_TENANT_LABEL_KEY]; const [timeRange, setTimeRange] = React.useState(); useEffect(() => { + if (!configLoaded) { + return; + } + if (rule?.query && tenant) { getLogs({ query: rule.query, timeRange, tenant, schema: getSchema(config.schema) }); } - }, [rule?.query, timeRange]); + }, [rule?.query, timeRange, configLoaded, tenant, config.schema]); const tenantError = !tenant ? new Error( diff --git a/web/src/components/logs-table.tsx b/web/src/components/logs-table.tsx index 3eacb13c2..35f3a72a3 100644 --- a/web/src/components/logs-table.tsx +++ b/web/src/components/logs-table.tsx @@ -169,15 +169,15 @@ const ResourceLinkList: React.FC<{ }; type TableRowProps = { - expandedItems: Set; + expandedItemsRef: React.MutableRefObject>; handleRowToggle: (e: React.MouseEvent, rowIndex: number) => void; showResources: boolean; colSpan?: number; }; -const TableRow = ({ expandedItems, handleRowToggle, showResources, colSpan }: TableRowProps) => { +const TableRow = ({ expandedItemsRef, handleRowToggle, showResources, colSpan }: TableRowProps) => { return function TableRowComponent({ obj, activeColumnIDs }: RowProps) { - const isExpanded = expandedItems.has(obj.logIndex); + const isExpanded = expandedItemsRef.current.has(obj.logIndex); return obj.type === 'log' ? ( <> @@ -235,6 +235,8 @@ export const LogsTable: React.FC = ({ schema, }) => { const [expandedItems, setExpandedItems] = React.useState>(new Set()); + const expandedItemsRef = React.useRef(expandedItems); + expandedItemsRef.current = expandedItems; const [prevChildrenCount, setPrevChildrenCount] = React.useState(0); const [sortBy, setSortBy] = React.useState({ index: 1, @@ -255,14 +257,17 @@ export const LogsTable: React.FC = ({ setPrevChildrenCount(React.Children.count(children)); }, [children]); - const handleRowToggle = (_event: React.MouseEvent, rowIndex: number) => { - if (expandedItems.has(rowIndex)) { - expandedItems.delete(rowIndex); - setExpandedItems(new Set(expandedItems)); - } else { - setExpandedItems(new Set(expandedItems.add(rowIndex))); - } - }; + const handleRowToggle = useCallback((_event: React.MouseEvent, rowIndex: number) => { + setExpandedItems((prev) => { + const next = new Set(prev); + if (next.has(rowIndex)) { + next.delete(rowIndex); + } else { + next.add(rowIndex); + } + return next; + }); + }, []); const getSortParams = useCallback( (columnIndex: number): ThProps['sort'] => { @@ -293,21 +298,36 @@ export const LogsTable: React.FC = ({ [sortBy, onSortByDate], ); - const sortedData = React.useMemo(() => { - setExpandedItems(new Set()); + const prevLogsDataRef = React.useRef(logsData); + if (logsData !== prevLogsDataRef.current) { + const prevData = prevLogsDataRef.current; + prevLogsDataRef.current = logsData; + + const dataChanged = + !prevData || + !logsData || + isStreaming || + prevData.data?.result?.length !== logsData.data?.result?.length; + + if (expandedItems.size > 0 && dataChanged) { + setExpandedItems(new Set()); + } + } + const sortedData = React.useMemo(() => { + const dataCopy = [...tableData]; if (sortBy.index !== undefined && columns[sortBy.index]) { const { sort } = columns[sortBy.index]; if (sort && typeof sort === 'function') { return sort( - tableData, + dataCopy, sortBy.direction === 'asc' ? SortByDirection.asc : SortByDirection.desc, ); } } - return tableData.sort((a, b) => numericComparator(a.timestamp, b.timestamp, -1)); - }, [tableData, columns, sortBy]); + return dataCopy.sort((a, b) => numericComparator(a.timestamp, b.timestamp, -1)); + }, [tableData, sortBy]); const dataIsEmpty = sortedData.length === 0; @@ -315,6 +335,29 @@ export const LogsTable: React.FC = ({ onLoadMore?.(tableData[tableData.length - 1].timestamp / 1e6); }; + const RowComponent = React.useMemo( + () => + TableRow({ + expandedItemsRef, + handleRowToggle, + showResources, + colSpan: columns.length, + }), + [handleRowToggle, showResources], + ); + + const getRowClassName = useCallback((row: LogTableData) => { + const expanded = expandedItemsRef.current.has(row.logIndex); + let expandedClass = ''; + if (expanded) { + expandedClass = + row.type === 'log' + ? 'lv-plugin__table__row--expanded' + : 'lv-plugin__table__row--expanded-details'; + } + return `lv-plugin__table__row ${getSeverityClass(row.severity)} ${expandedClass}`; + }, []); + return (
{showStats && } @@ -322,23 +365,10 @@ export const LogsTable: React.FC = ({ - `lv-plugin__table__row ${getSeverityClass(row.severity)} ${ - expandedItems.has(row.logIndex) - ? row.type === 'log' - ? 'lv-plugin__table__row--expanded' - : 'lv-plugin__table__row--expanded-details' - : '' - }` - } + getRowClassName={getRowClassName} error={error} isLoading={isLoading} isStreaming={isStreaming} @@ -349,6 +379,8 @@ export const LogsTable: React.FC = ({ shouldResize={showStats || React.Children.count(children) != prevChildrenCount} hasNamespaceFilter={hasNamespaceFilter} schema={schema} + expandedItems={expandedItems} + showResources={showResources} />
); diff --git a/web/src/components/refresh-interval-dropdown.tsx b/web/src/components/refresh-interval-dropdown.tsx index 42726bd4a..8c725d183 100644 --- a/web/src/components/refresh-interval-dropdown.tsx +++ b/web/src/components/refresh-interval-dropdown.tsx @@ -46,8 +46,10 @@ export const RefreshIntervalDropdown: React.FC = ( ? parseInt(storedRefreshInterval.interval, 10) : 0, ); - const [delay, setDelay] = React.useState(0); + const [delay, setDelay] = React.useState(refreshIntervalOptions[selectedIndex].delay); const timer = React.useRef(null); + const onRefreshRef = React.useRef(onRefresh); + onRefreshRef.current = onRefresh; const clearTimer = () => { if (timer.current) { @@ -63,23 +65,16 @@ export const RefreshIntervalDropdown: React.FC = ( setStoredRefreshInterval({ interval: index.toString(10) }); }; - const restartTimer = (callRefreshImmediately = true) => { + React.useEffect(() => { clearTimer(); if (delay !== 0) { - if (callRefreshImmediately) { - onRefresh?.(); - } - timer.current = setInterval(() => onRefresh?.(), delay); + onRefreshRef.current?.(); + timer.current = setInterval(() => onRefreshRef.current?.(), delay); } return () => clearTimer(); - }; - - React.useEffect(() => restartTimer(), [delay]); - - // Avoid calling refresh immediately when onRefresh callback has changed - React.useEffect(() => restartTimer(false), [onRefresh]); + }, [delay]); const toggleIsOpen = () => { setIsOpen(!isOpen); diff --git a/web/src/components/virtualized-logs-table.tsx b/web/src/components/virtualized-logs-table.tsx index d9c7333e6..fcfaaa2b8 100644 --- a/web/src/components/virtualized-logs-table.tsx +++ b/web/src/components/virtualized-logs-table.tsx @@ -29,6 +29,8 @@ interface VirtualizedLogsTableProps { csvData?: string; hasNamespaceFilter?: boolean; schema: Schema; + expandedItems?: Set; + showResources?: boolean; } export type TableRowProps = { @@ -95,6 +97,8 @@ type VirtualizedTableBodyProps = { getRowTitle?: (obj: D) => string; getRowClassName?: (obj: D) => string; scrollToIndex?: number; + expandedItems?: Set; + showResources?: boolean; }; const TableRow: React.FC = ({ id, index, trKey, style, className, ...props }) => { @@ -127,13 +131,31 @@ const VirtualizedTableBody = ({ getRowTitle, getRowClassName, scrollToIndex, + expandedItems, + showResources, }: // eslint-disable-next-line @typescript-eslint/no-explicit-any VirtualizedTableBodyProps) => { - const cellMeasurementCache = new CellMeasurerCache({ - fixedWidth: true, - minHeight: 1, - keyMapper: (rowIndex) => rowIndex, - }); + const cellMeasurementCache = React.useMemo( + () => + new CellMeasurerCache({ + fixedWidth: true, + minHeight: 1, + keyMapper: (rowIndex) => rowIndex, + }), + [], + ); + + const tableBodyRef = React.useRef(null); + const isFirstRender = React.useRef(true); + + useEffect(() => { + if (isFirstRender.current) { + isFirstRender.current = false; + return; + } + cellMeasurementCache.clearAll(); + tableBodyRef.current?.forceUpdateVirtualGrid(); + }, [expandedItems, showResources]); const activeColumnIDs = React.useMemo(() => new Set(columns.map((c) => c.id)), [columns]); @@ -186,6 +208,7 @@ VirtualizedTableBodyProps) => { return ( ) => { const { t } = useTranslation('plugin__logging-view-plugin'); const colSpan = columns.length + 3; @@ -350,6 +375,8 @@ export const VirtualizedLogsTable = ({ width={width} getRowClassName={getRowClassName} scrollToIndex={scrollToIndex} + expandedItems={expandedItems} + showResources={showResources} /> )} diff --git a/web/src/hooks/LogsConfigProvider.tsx b/web/src/hooks/LogsConfigProvider.tsx index c30f308d4..002cbdb6f 100644 --- a/web/src/hooks/LogsConfigProvider.tsx +++ b/web/src/hooks/LogsConfigProvider.tsx @@ -1,10 +1,9 @@ -import React, { createContext, useCallback, useContext, useEffect, useState } from 'react'; +import React, { createContext, useContext, useEffect, useMemo, useState } from 'react'; import { defaultConfig, getConfig } from '../backend-client'; import { Config } from '../logs.types'; interface LogsContextType { config: Config; - fetchConfig: () => Promise; configLoaded: boolean; } @@ -16,31 +15,27 @@ export const LogsConfigProvider: React.FC<{ children?: React.ReactNode | undefin const [config, setConfig] = useState(defaultConfig); const [configLoaded, setConfigLoaded] = useState(false); - const fetchConfig = useCallback(async () => { - try { - if (!configLoaded) { + useEffect(() => { + const loadConfig = async () => { + try { const configData = await getConfig(); const mergedConfig = { ...defaultConfig, ...configData }; setConfig(mergedConfig); setConfigLoaded(true); - - return mergedConfig; + } catch (error) { + // eslint-disable-next-line no-console + console.error('Error fetching logging plugin configuration', error); + setConfig(defaultConfig); + setConfigLoaded(true); } + }; + + loadConfig(); + }, []); - return config; - } catch (error) { - // eslint-disable-next-line no-console - console.error('Error fetching logging plugin configuration', error); - setConfig(defaultConfig); - return defaultConfig; - } - }, [config]); - - return ( - - {children} - - ); + const contextValue = useMemo(() => ({ config, configLoaded }), [config, configLoaded]); + + return {children}; }; export const useLogsConfig = (): LogsContextType => { @@ -50,9 +45,5 @@ export const useLogsConfig = (): LogsContextType => { throw new Error('useLogsConfig must be used within a LogsConfigProvider'); } - useEffect(() => { - context.fetchConfig(); - }, []); - return context; }; diff --git a/web/src/hooks/useLogs.ts b/web/src/hooks/useLogs.ts index 3f197de8a..de1a5b3f0 100644 --- a/web/src/hooks/useLogs.ts +++ b/web/src/hooks/useLogs.ts @@ -269,7 +269,8 @@ export const useLogs = ( throw new Error('useLogs must be used within a LogsProvider'); } - const { fetchConfig } = logsContext; + const configRef = React.useRef(logsContext.config); + configRef.current = logsContext.config; const [ { @@ -336,7 +337,7 @@ export const useLogs = ( logsAbort.current(); } - const config = await fetchConfig(); + const config = configRef.current; const { request, abort } = executeQueryRange({ query, @@ -405,7 +406,7 @@ export const useLogs = ( logsAbort.current(); } - const config = await fetchConfig(); + const config = configRef.current; const { request, abort } = executeQueryRange({ query, @@ -550,7 +551,7 @@ export const useLogs = ( volumeAbort.current(); } - const config = await fetchConfig(); + const config = configRef.current; // Volume API only accepts labels, so have to extract them from the query. // Only grabs the data within the { } @@ -626,7 +627,7 @@ export const useLogs = ( const { start, end } = numericTimeRange(currentTimeRange.current); - const config = await fetchConfig(); + const config = configRef.current; const { request, abort } = executeHistogramQuery({ query, diff --git a/web/src/pages/logs-detail-page.tsx b/web/src/pages/logs-detail-page.tsx index 19be9587e..4fac7406f 100644 --- a/web/src/pages/logs-detail-page.tsx +++ b/web/src/pages/logs-detail-page.tsx @@ -132,6 +132,8 @@ const LogsDetailPage: React.FC = ({ }; const runQuery = () => { + if (!configLoaded) return; + getLogs({ query, tenant: tenant.current, namespace, timeRange, direction, schema }); if (isHistogramVisible) { @@ -317,12 +319,13 @@ const LogsDetailPage: React.FC = ({ ); }; -const LogsDetailPageWrapper: React.FC = (props) => { +const LogsDetailPageWrapper: React.FC = React.memo((props) => { return ( ); -}; +}); +LogsDetailPageWrapper.displayName = 'LogsDetailPageWrapper'; export default LogsDetailPageWrapper; diff --git a/web/src/pages/logs-dev-page.tsx b/web/src/pages/logs-dev-page.tsx index d09ae8e1a..86f241939 100644 --- a/web/src/pages/logs-dev-page.tsx +++ b/web/src/pages/logs-dev-page.tsx @@ -109,6 +109,8 @@ const LogsDevPage: React.FC = ({ ns: namespaceFromProps }) => }; const runQuery = ({ queryToUse }: { queryToUse?: string } = {}) => { + if (!configLoaded) return; + getLogs({ query: queryToUse ?? query, timeRange, @@ -346,12 +348,13 @@ const LogsDevPage: React.FC = ({ ns: namespaceFromProps }) => ); }; -const LogsDevPageWrapper: React.FC = (props) => { +const LogsDevPageWrapper: React.FC = React.memo((props) => { return ( ); -}; +}); +LogsDevPageWrapper.displayName = 'LogsDevPageWrapper'; export default LogsDevPageWrapper; diff --git a/web/src/pages/logs-page.tsx b/web/src/pages/logs-page.tsx index 785761d78..2af48f652 100644 --- a/web/src/pages/logs-page.tsx +++ b/web/src/pages/logs-page.tsx @@ -102,6 +102,8 @@ const LogsPage: React.FC = () => { }; const runQuery = ({ queryToUse }: { queryToUse?: string } = {}) => { + if (!configLoaded) return; + getLogs({ query: queryToUse ?? query, tenant, timeRange, direction, schema }); if (isHistogramVisible) { @@ -306,12 +308,13 @@ const LogsPage: React.FC = () => { ); }; -const LogsPageWrapper: React.FC = () => { +const LogsPageWrapper: React.FC = React.memo(() => { return ( ); -}; +}); +LogsPageWrapper.displayName = 'LogsPageWrapper'; export default LogsPageWrapper;