New Logs Context: Add time window option (#109901)
* LogLineDetails: add open and close events * LogLineContext: add interval picker * Log Context: allow to customize the time window around the log * Loki: support custom time window * LogLineContext: implement variable time window * Remove console * LogLineContext: store and format options * LogLineContext: replace radio with combobox * LogLineContext: run time window on the first request * InfiniteScroll: use loading to prevent fake events * Chore: update old comment * Minor reorg * Clean up unnecessary styles * LogLineContext: fix overflow and scroll * LogList: fix loading prop * Update comment * Translations * Update public/app/features/logs/components/panel/LogLineContext.tsx * LogLineContext: move default to constant * LogLineContext: add test * Prettier * New Logs Context: Generic DS support for time window option (#109934) * chore: add supportsAdjustableWindow to logs context interface * Build --------- Co-authored-by: Galen Kistler <109082771+gtk-grafana@users.noreply.github.com>
This commit is contained in:
co-authored by
Galen Kistler
parent
2b254ed623
commit
01d48e26fe
@@ -137,6 +137,8 @@ export interface LogRowContextOptions {
|
||||
direction?: LogRowContextQueryDirection;
|
||||
limit?: number;
|
||||
scopedVars?: ScopedVars;
|
||||
// Optional. Size of the time window to get logs before of after the referenced entry.
|
||||
timeWindowMs?: number;
|
||||
}
|
||||
|
||||
export enum LogRowContextQueryDirection {
|
||||
@@ -181,6 +183,9 @@ export interface DataSourceWithLogsContextSupport<TQuery extends DataQuery = Dat
|
||||
origQuery?: TQuery,
|
||||
scopedVars?: ScopedVars
|
||||
): React.ReactNode;
|
||||
|
||||
// Does the datasource support the user adjusting the time range in the logs context window? https://github.com/grafana/grafana/pull/109901
|
||||
supportsAdjustableWindow?: boolean;
|
||||
}
|
||||
|
||||
export const hasLogsContextSupport = (datasource: unknown): datasource is DataSourceWithLogsContextSupport => {
|
||||
|
||||
@@ -581,10 +581,12 @@ const UnthemedLogs: React.FunctionComponent<Props> = (props: Props) => {
|
||||
let onCloseContext = useCallback(() => {
|
||||
setContextOpen(false);
|
||||
setContextRow(undefined);
|
||||
reportInteraction('grafana_explore_logs_log_context_closed', {
|
||||
datasourceType: contextRow?.datasourceType,
|
||||
logRowUid: contextRow?.uid,
|
||||
});
|
||||
if (!config.featureToggles.newLogContext) {
|
||||
reportInteraction('grafana_explore_logs_log_context_closed', {
|
||||
datasourceType: contextRow?.datasourceType,
|
||||
logRowUid: contextRow?.uid,
|
||||
});
|
||||
}
|
||||
onCloseCallbackRef?.current();
|
||||
}, [contextRow?.datasourceType, contextRow?.uid, onCloseCallbackRef]);
|
||||
|
||||
@@ -592,10 +594,12 @@ const UnthemedLogs: React.FunctionComponent<Props> = (props: Props) => {
|
||||
// we are setting the `contextOpen` open state and passing it down to the `LogRow` in order to highlight the row when a LogContext is open
|
||||
setContextOpen(true);
|
||||
setContextRow(row);
|
||||
reportInteraction('grafana_explore_logs_log_context_opened', {
|
||||
datasourceType: row.datasourceType,
|
||||
logRowUid: row.uid,
|
||||
});
|
||||
if (!config.featureToggles.newLogContext) {
|
||||
reportInteraction('grafana_explore_logs_log_context_opened', {
|
||||
datasourceType: row.datasourceType,
|
||||
logRowUid: row.uid,
|
||||
});
|
||||
}
|
||||
onCloseCallbackRef.current = onClose;
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ export interface Props {
|
||||
displayedFields: string[];
|
||||
handleOverflow: (index: number, id: string, height?: number) => void;
|
||||
infiniteScrollMode: InfiniteScrollMode;
|
||||
loading?: boolean;
|
||||
loadMore?: LoadMoreLogsType;
|
||||
logs: LogListModel[];
|
||||
onClick: (e: MouseEvent<HTMLElement>, log: LogListModel) => void;
|
||||
@@ -50,6 +51,7 @@ export const InfiniteScroll = ({
|
||||
displayedFields,
|
||||
handleOverflow,
|
||||
infiniteScrollMode,
|
||||
loading,
|
||||
loadMore,
|
||||
logs,
|
||||
onClick,
|
||||
@@ -102,12 +104,12 @@ export const InfiniteScroll = ({
|
||||
}, [prevSortOrder, sortOrder]);
|
||||
|
||||
useEffect(() => {
|
||||
if (autoScroll) {
|
||||
if (autoScroll && !loading) {
|
||||
setInitialScrollPosition(scrollToLogLineRef.current);
|
||||
scrollToLogLineRef.current = undefined;
|
||||
setAutoScroll(false);
|
||||
}
|
||||
}, [autoScroll, setInitialScrollPosition]);
|
||||
}, [autoScroll, loading, setInitialScrollPosition]);
|
||||
|
||||
const onLoadMore = useCallback(
|
||||
(scrollDirection: ScrollDirection) => {
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
|
||||
import { dataFrameToLogsModel } from '../../logsModel';
|
||||
|
||||
import { LogLineContext } from './LogLineContext';
|
||||
import { DEFAULT_TIME_WINDOW, LogLineContext, PAGE_SIZE } from './LogLineContext';
|
||||
|
||||
jest.mock('@grafana/assistant', () => ({
|
||||
...jest.requireActual('@grafana/assistant'),
|
||||
@@ -542,4 +542,31 @@ describe('LogLineContext', () => {
|
||||
|
||||
await waitFor(() => expect(dispatchMock).toHaveBeenCalledWith(splitOpenSym));
|
||||
});
|
||||
|
||||
test('Allows to change the time window surrounding the log', async () => {
|
||||
row.datasourceType = 'loki';
|
||||
|
||||
render(
|
||||
<LogLineContext
|
||||
log={row}
|
||||
open={true}
|
||||
onClose={() => {}}
|
||||
getRowContext={getRowContext}
|
||||
timeZone={timeZone}
|
||||
sortOrder={LogsSortOrder.Descending}
|
||||
/>
|
||||
);
|
||||
await waitFor(() =>
|
||||
expect(getRowContext).toHaveBeenCalledWith(expect.anything(), {
|
||||
limit: PAGE_SIZE,
|
||||
direction: LogRowContextQueryDirection.Forward,
|
||||
timeWindowMs: DEFAULT_TIME_WINDOW,
|
||||
})
|
||||
);
|
||||
expect(getRowContext).toHaveBeenCalledWith(expect.anything(), {
|
||||
limit: PAGE_SIZE,
|
||||
direction: LogRowContextQueryDirection.Backward,
|
||||
timeWindowMs: DEFAULT_TIME_WINDOW,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,26 +3,30 @@ import { partition } from 'lodash';
|
||||
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
|
||||
import {
|
||||
AbsoluteTimeRange,
|
||||
CoreApp,
|
||||
DataQueryResponse,
|
||||
DataSourceApi,
|
||||
DataSourceWithLogsContextSupport,
|
||||
dateTime,
|
||||
EventBusSrv,
|
||||
formattedValueToString,
|
||||
getValueFormat,
|
||||
GrafanaTheme2,
|
||||
hasLogsContextSupport,
|
||||
LoadingState,
|
||||
LogRowContextOptions,
|
||||
LogRowContextQueryDirection,
|
||||
LogRowModel,
|
||||
LogsDedupStrategy,
|
||||
LogsSortOrder,
|
||||
dateTime,
|
||||
TimeRange,
|
||||
LoadingState,
|
||||
CoreApp,
|
||||
LogRowModel,
|
||||
AbsoluteTimeRange,
|
||||
EventBusSrv,
|
||||
store,
|
||||
TimeRange,
|
||||
} from '@grafana/data';
|
||||
import { Trans, t } from '@grafana/i18n';
|
||||
import { config, reportInteraction } from '@grafana/runtime';
|
||||
import { t, Trans } from '@grafana/i18n';
|
||||
import { config, getDataSourceSrv, reportInteraction } from '@grafana/runtime';
|
||||
import { DataQuery, TimeZone } from '@grafana/schema';
|
||||
import { Button, Collapse, Modal, useTheme2 } from '@grafana/ui';
|
||||
import { Button, Collapse, Combobox, ComboboxOption, InlineLabel, Modal, Stack, useTheme2 } from '@grafana/ui';
|
||||
import { splitOpen } from 'app/features/explore/state/main';
|
||||
import { useDispatch } from 'app/types/store';
|
||||
|
||||
@@ -56,7 +60,8 @@ interface LogLineContextProps {
|
||||
onClickHideField?: (key: string) => void;
|
||||
}
|
||||
|
||||
const PAGE_SIZE = 100;
|
||||
export const PAGE_SIZE = 100;
|
||||
export const DEFAULT_TIME_WINDOW = 7200000;
|
||||
|
||||
export const LogLineContext = memo(
|
||||
({
|
||||
@@ -84,6 +89,14 @@ export const LogLineContext = memo(
|
||||
const [aboveState, setAboveState] = useState(LoadingState.NotStarted);
|
||||
const [belowState, setBelowState] = useState(LoadingState.NotStarted);
|
||||
const [showLog, setShowLog] = useState(false);
|
||||
const [datasourceInstance, setDatasourceInstance] = useState<
|
||||
(DataSourceApi & DataSourceWithLogsContextSupport) | null
|
||||
>(null);
|
||||
const defaultTimeWindow = logOptionsStorageKey
|
||||
? (store.get(`${logOptionsStorageKey}.contextTimeWindow`) ?? DEFAULT_TIME_WINDOW.toString())
|
||||
: DEFAULT_TIME_WINDOW.toString();
|
||||
const [timeWindow, setTimeWindow] = useState(parseInt(defaultTimeWindow, 10));
|
||||
|
||||
const eventBusRef = useRef(new EventBusSrv());
|
||||
|
||||
const dispatch = useDispatch();
|
||||
@@ -95,8 +108,7 @@ export const LogLineContext = memo(
|
||||
sortOrder === LogsSortOrder.Ascending ? allLogs[0].timeEpochMs : allLogs[allLogs.length - 1].timeEpochMs;
|
||||
let toMs =
|
||||
sortOrder === LogsSortOrder.Ascending ? allLogs[allLogs.length - 1].timeEpochMs : allLogs[0].timeEpochMs;
|
||||
// In case we have a lot of logs and from and to have same millisecond
|
||||
// we add 1 millisecond to toMs to make sure we have a range
|
||||
// Add one millisecond to get a range when from and to are equal.
|
||||
if (fromMs === toMs) {
|
||||
toMs += 1;
|
||||
}
|
||||
@@ -119,24 +131,23 @@ export const LogLineContext = memo(
|
||||
setContextQuery(contextQuery);
|
||||
}, [log, getRowContextQuery]);
|
||||
|
||||
const updateResults = useCallback(async () => {
|
||||
setAboveLogs([]);
|
||||
setBelowLogs([]);
|
||||
await updateContextQuery();
|
||||
setInitialized(false);
|
||||
}, [updateContextQuery]);
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
updateContextQuery();
|
||||
reportInteraction('logs_log_line_context_open', {
|
||||
datasourceType: log.datasourceType,
|
||||
uid: log.uid,
|
||||
});
|
||||
}
|
||||
}, [updateContextQuery, open]);
|
||||
}, [updateContextQuery, open, log]);
|
||||
|
||||
const getContextLogs = useCallback(
|
||||
async (place: 'above' | 'below', refLog: LogRowModel): Promise<LogRowModel[]> => {
|
||||
async (place: 'above' | 'below', refLog: LogRowModel, timeWindowMs?: number): Promise<LogRowModel[]> => {
|
||||
const result = await getRowContext(normalizeLogRefId(refLog), {
|
||||
limit: PAGE_SIZE,
|
||||
direction: getLoadMoreDirection(place, sortOrder),
|
||||
// Only on the initial request
|
||||
timeWindowMs,
|
||||
});
|
||||
|
||||
const newLogs = dataFrameToLogsModel(result.data).rows;
|
||||
@@ -149,12 +160,12 @@ export const LogLineContext = memo(
|
||||
);
|
||||
|
||||
const loadMore = useCallback(
|
||||
async (place: 'above' | 'below', refLog: LogRowModel) => {
|
||||
async (place: 'above' | 'below', refLog: LogRowModel, timeWindow?: number) => {
|
||||
const setState = place === 'above' ? setAboveState : setBelowState;
|
||||
setState(LoadingState.Loading);
|
||||
|
||||
try {
|
||||
const newLogs = (await getContextLogs(place, refLog)).map((r) =>
|
||||
const newLogs = (await getContextLogs(place, refLog, timeWindow)).map((r) =>
|
||||
// apply the original row's searchWords to all the rows for highlighting
|
||||
!r.searchWords || !r.searchWords?.length ? { ...r, searchWords: log.searchWords } : r
|
||||
);
|
||||
@@ -188,10 +199,10 @@ export const LogLineContext = memo(
|
||||
return;
|
||||
}
|
||||
if (!initialized) {
|
||||
Promise.all([loadMore('above', log), loadMore('below', log)]).then(() => {});
|
||||
Promise.all([loadMore('above', log, timeWindow), loadMore('below', log, timeWindow)]);
|
||||
setInitialized(true);
|
||||
}
|
||||
}, [initialized, loadMore, log, open]);
|
||||
}, [initialized, loadMore, log, open, timeWindow]);
|
||||
|
||||
const handleLoadMore = useCallback(
|
||||
(_: AbsoluteTimeRange, direction: ScrollDirection) => {
|
||||
@@ -212,10 +223,70 @@ export const LogLineContext = memo(
|
||||
);
|
||||
}, [log.uid]);
|
||||
|
||||
const onSplitViewClick = useCallback(() => {
|
||||
if (!contextQuery) {
|
||||
return;
|
||||
}
|
||||
let rowId = log.uid;
|
||||
if (log.dataFrame.refId) {
|
||||
// the orignal row has the refid from the base query and not the refid from the context query, so we need to replace it.
|
||||
rowId = log.uid.replace(log.dataFrame.refId, contextQuery.refId);
|
||||
}
|
||||
|
||||
dispatch(
|
||||
splitOpen({
|
||||
queries: [contextQuery],
|
||||
range: timeRange,
|
||||
datasourceUid: contextQuery.datasource!.uid!,
|
||||
panelsState: {
|
||||
logs: {
|
||||
id: rowId,
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
onClose();
|
||||
reportInteraction('logs_log_line_context_open_in_split_clicked', {
|
||||
datasourceType: log.datasourceType,
|
||||
});
|
||||
}, [contextQuery, dispatch, log.dataFrame.refId, log.datasourceType, log.uid, onClose, timeRange]);
|
||||
|
||||
const handleTimeWindowChange = useCallback(
|
||||
(option: ComboboxOption<string>) => {
|
||||
if (logOptionsStorageKey) {
|
||||
store.set(`${logOptionsStorageKey}.contextTimeWindow`, option.value);
|
||||
}
|
||||
setTimeWindow(parseInt(option.value, 10));
|
||||
setAboveLogs([]);
|
||||
setBelowLogs([]);
|
||||
setInitialized(false);
|
||||
reportInteraction('logs_log_line_context_time_window_change', {
|
||||
window_size: option.value,
|
||||
});
|
||||
},
|
||||
[logOptionsStorageKey]
|
||||
);
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
reportInteraction('logs_log_line_context_closed', {
|
||||
datasourceType: log.datasourceType,
|
||||
uid: log.uid,
|
||||
});
|
||||
onClose();
|
||||
}, [log.datasourceType, log.uid, onClose]);
|
||||
|
||||
const updateResults = useCallback(async () => {
|
||||
setAboveLogs([]);
|
||||
setBelowLogs([]);
|
||||
await updateContextQuery();
|
||||
setInitialized(false);
|
||||
}, [updateContextQuery]);
|
||||
|
||||
const wrapLogMessage = logOptionsStorageKey ? store.getBool(`${logOptionsStorageKey}.wrapLogMessage`, true) : true;
|
||||
const syntaxHighlighting = logOptionsStorageKey
|
||||
? store.getBool(`${logOptionsStorageKey}.syntaxHighlighting`, true)
|
||||
: true;
|
||||
|
||||
// @todo: Remove when the LogRows are deprecated
|
||||
const logListModel = useMemo(
|
||||
() =>
|
||||
@@ -229,16 +300,28 @@ export const LogLineContext = memo(
|
||||
[log, timeZone, wrapLogMessage]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (log.datasourceUid) {
|
||||
getDataSourceSrv()
|
||||
.get({ uid: log.datasourceUid })
|
||||
.then((ds) => {
|
||||
if (hasLogsContextSupport(ds)) {
|
||||
setDatasourceInstance(ds);
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [log.datasourceUid]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={open}
|
||||
title={t('logs.log-line-context.title-log-context', 'Log context')}
|
||||
contentClassName={styles.flexColumn}
|
||||
className={styles.modal}
|
||||
onDismiss={onClose}
|
||||
onDismiss={handleClose}
|
||||
>
|
||||
{config.featureToggles.logsContextDatasourceUi && getLogRowContextUi && (
|
||||
<div className={styles.datasourceUi}>{getLogRowContextUi(log, updateResults)}</div>
|
||||
<div>{getLogRowContextUi(log, updateResults)}</div>
|
||||
)}
|
||||
<Collapse
|
||||
collapsible={true}
|
||||
@@ -249,6 +332,38 @@ export const LogLineContext = memo(
|
||||
>
|
||||
<LogLineDetailsLog log={logListModel} syntaxHighlighting={syntaxHighlighting} />
|
||||
</Collapse>
|
||||
<div className={styles.controls}>
|
||||
{datasourceInstance?.supportsAdjustableWindow && (
|
||||
<Stack>
|
||||
<InlineLabel
|
||||
htmlFor="time-window-control"
|
||||
tooltip={t(
|
||||
'logs.log-line-context.time-window-tooltip',
|
||||
'Amount of time before and after the referenced log'
|
||||
)}
|
||||
width="auto"
|
||||
>
|
||||
{t('logs.log-line-context.time-window-label', 'Context time window')}
|
||||
</InlineLabel>
|
||||
<Combobox
|
||||
id="time-window-control"
|
||||
options={getTimeWindowOptions()}
|
||||
onChange={handleTimeWindowChange}
|
||||
value={timeWindow.toString()}
|
||||
minWidth={5}
|
||||
width="auto"
|
||||
/>
|
||||
</Stack>
|
||||
)}
|
||||
<Button variant="secondary" onClick={onScrollCenterClick}>
|
||||
<Trans i18nKey="logs.log-line-context.center-matched-line">Center matched line</Trans>
|
||||
</Button>
|
||||
{contextQuery?.datasource?.uid && (
|
||||
<Button variant="secondary" onClick={onSplitViewClick}>
|
||||
<Trans i18nKey="logs.log-line-context.open-in-split-view">Open in split view</Trans>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.loadingIndicator}>
|
||||
{aboveState === LoadingState.Loading && (
|
||||
<LoadingIndicator
|
||||
@@ -306,43 +421,6 @@ export const LogLineContext = memo(
|
||||
<Trans i18nKey="logs.log-line-context.no-more-logs-available">No more logs available.</Trans>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Modal.ButtonRow>
|
||||
<Button variant="secondary" onClick={onScrollCenterClick}>
|
||||
<Trans i18nKey="logs.log-line-context.center-matched-line">Center matched line</Trans>
|
||||
</Button>
|
||||
{contextQuery?.datasource?.uid && (
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={async () => {
|
||||
let rowId = log.uid;
|
||||
if (log.dataFrame.refId) {
|
||||
// the orignal row has the refid from the base query and not the refid from the context query, so we need to replace it.
|
||||
rowId = log.uid.replace(log.dataFrame.refId, contextQuery.refId);
|
||||
}
|
||||
|
||||
dispatch(
|
||||
splitOpen({
|
||||
queries: [contextQuery],
|
||||
range: timeRange,
|
||||
datasourceUid: contextQuery.datasource!.uid!,
|
||||
panelsState: {
|
||||
logs: {
|
||||
id: rowId,
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
onClose();
|
||||
reportInteraction('logs_log_line_context_open_in_split_clicked', {
|
||||
datasourceType: log.datasourceType,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Trans i18nKey="logs.log-line-context.open-in-split-view">Open in split view</Trans>
|
||||
</Button>
|
||||
)}
|
||||
</Modal.ButtonRow>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -362,10 +440,6 @@ const getStyles = (theme: GrafanaTheme2) => {
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
}),
|
||||
datasourceUi: css({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
}),
|
||||
loadingIndicator: css({
|
||||
height: theme.spacing(3),
|
||||
minHeight: theme.spacing(3),
|
||||
@@ -377,8 +451,8 @@ const getStyles = (theme: GrafanaTheme2) => {
|
||||
wrapper: css({
|
||||
border: `1px solid ${theme.colors.border.weak}`,
|
||||
padding: theme.spacing(0, 1, 1, 0),
|
||||
flex: 1,
|
||||
height: '100%',
|
||||
flex: '1 1 auto',
|
||||
minHeight: 0,
|
||||
}),
|
||||
logsContainer: css({
|
||||
height: '100%',
|
||||
@@ -389,6 +463,7 @@ const getStyles = (theme: GrafanaTheme2) => {
|
||||
flexDirection: 'column',
|
||||
padding: theme.spacing(0, 3, 3, 3),
|
||||
height: '100%',
|
||||
gap: theme.spacing(0.5),
|
||||
}),
|
||||
link: css({
|
||||
color: theme.colors.text.secondary,
|
||||
@@ -404,6 +479,11 @@ const getStyles = (theme: GrafanaTheme2) => {
|
||||
width: '75vw',
|
||||
whiteSpace: 'nowrap',
|
||||
}),
|
||||
controls: css({
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
gap: theme.spacing(2),
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -445,3 +525,11 @@ const normalizeLogRefId = (log: LogRowModel): LogRowModel => {
|
||||
const containsRow = (rows: LogRowModel[], row: LogRowModel) => {
|
||||
return rows.some((r) => r.entry === row.entry && r.timeEpochNs === row.timeEpochNs);
|
||||
};
|
||||
|
||||
function getTimeWindowOptions() {
|
||||
const intervals = [100, 500, 1000, 5000, 30000, 60000, 300000, 1800000, 3600000, DEFAULT_TIME_WINDOW];
|
||||
return intervals.map((interval) => ({
|
||||
label: formattedValueToString(getValueFormat('ms')(interval)),
|
||||
value: interval.toString(),
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -96,7 +96,6 @@ type LogListComponentProps = Omit<
|
||||
| 'dedupStrategy'
|
||||
| 'displayedFields'
|
||||
| 'enableLogDetails'
|
||||
| 'loading'
|
||||
| 'logOptionsStorageKey'
|
||||
| 'permalinkedLogId'
|
||||
| 'showTime'
|
||||
@@ -203,6 +202,7 @@ export const LogList = ({
|
||||
grammar={grammar}
|
||||
initialScrollPosition={initialScrollPosition}
|
||||
infiniteScrollMode={infiniteScrollMode}
|
||||
loading={loading}
|
||||
loadMore={loadMore}
|
||||
logs={logs}
|
||||
showControls={showControls}
|
||||
@@ -221,6 +221,7 @@ const LogListComponent = ({
|
||||
grammar,
|
||||
initialScrollPosition = 'top',
|
||||
infiniteScrollMode = 'interval',
|
||||
loading,
|
||||
loadMore,
|
||||
logs,
|
||||
showControls,
|
||||
@@ -452,6 +453,7 @@ const LogListComponent = ({
|
||||
displayedFields={displayedFields}
|
||||
handleOverflow={handleOverflow}
|
||||
infiniteScrollMode={infiniteScrollMode}
|
||||
loading={loading}
|
||||
logs={filteredLogs}
|
||||
loadMore={loadMore}
|
||||
onClick={handleLogLineClick}
|
||||
|
||||
@@ -71,7 +71,7 @@ export class LogContextProvider {
|
||||
this.cachedContextFilters = filters;
|
||||
}
|
||||
|
||||
return await this.prepareLogRowContextQueryTarget(row, limit, direction, origQuery);
|
||||
return await this.prepareLogRowContextQueryTarget(row, limit, direction, origQuery, options?.timeWindowMs);
|
||||
}
|
||||
|
||||
getLogRowContextQuery = async (
|
||||
@@ -136,12 +136,11 @@ export class LogContextProvider {
|
||||
row: LogRowModel,
|
||||
limit: number,
|
||||
direction: LogRowContextQueryDirection,
|
||||
origQuery?: LokiQuery
|
||||
origQuery?: LokiQuery,
|
||||
timeWindowMs = 2 * 60 * 60 * 1000
|
||||
): Promise<{ query: LokiQuery; range: TimeRange }> {
|
||||
const expr = this.prepareExpression(this.cachedContextFilters, origQuery);
|
||||
|
||||
const contextTimeBuffer = 2 * 60 * 60 * 1000; // 2h buffer
|
||||
|
||||
const queryDirection =
|
||||
direction === LogRowContextQueryDirection.Forward ? LokiQueryDirection.Forward : LokiQueryDirection.Backward;
|
||||
|
||||
@@ -174,11 +173,11 @@ export class LogContextProvider {
|
||||
// because the are before but came it he response that should return only rows after.
|
||||
from: timestamp,
|
||||
// convert to ns, we lose some precision here but it is not that important at the far points of the context
|
||||
to: toUtc(row.timeEpochMs + contextTimeBuffer),
|
||||
to: toUtc(row.timeEpochMs + timeWindowMs),
|
||||
}
|
||||
: {
|
||||
// convert to ns, we lose some precision here but it is not that important at the far points of the context
|
||||
from: toUtc(row.timeEpochMs - contextTimeBuffer),
|
||||
from: toUtc(row.timeEpochMs - timeWindowMs),
|
||||
to: timestamp,
|
||||
};
|
||||
|
||||
|
||||
@@ -162,8 +162,12 @@ export class LokiDatasource
|
||||
};
|
||||
this.variables = new LokiVariableSupport(this);
|
||||
this.logContextProvider = new LogContextProvider(this);
|
||||
this.supportsAdjustableWindow = true;
|
||||
}
|
||||
|
||||
// Flag marking datasource as supporting adjusting the time range window in the logs context window: https://github.com/grafana/grafana/pull/109901
|
||||
public supportsAdjustableWindow;
|
||||
|
||||
/**
|
||||
* Implemented for DataSourceWithSupplementaryQueriesSupport.
|
||||
* It generates a DataQueryRequest for a specific supplementary query type.
|
||||
|
||||
@@ -9482,6 +9482,8 @@
|
||||
"no-more-logs-available": "No more logs available.",
|
||||
"older-logs": "older",
|
||||
"open-in-split-view": "Open in split view",
|
||||
"time-window-label": "Context time window",
|
||||
"time-window-tooltip": "Amount of time before and after the referenced log",
|
||||
"title-log-context": "Log context",
|
||||
"title-log-line": "Referenced log line"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user