mirror of
https://github.com/grafana/grafana.git
synced 2025-02-13 00:55:47 -06:00
252 lines
8.8 KiB
TypeScript
252 lines
8.8 KiB
TypeScript
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
import useAsync from 'react-use/lib/useAsync';
|
|
|
|
import {
|
|
DataQueryError,
|
|
DataQueryResponse,
|
|
Field,
|
|
FieldCache,
|
|
LogRowModel,
|
|
LogsSortOrder,
|
|
toDataFrame,
|
|
} from '@grafana/data';
|
|
import { reportInteraction } from '@grafana/runtime';
|
|
|
|
export interface RowContextOptions {
|
|
direction?: 'BACKWARD' | 'FORWARD';
|
|
limit?: number;
|
|
}
|
|
|
|
export interface LogRowContextRows {
|
|
before?: string[];
|
|
after?: string[];
|
|
}
|
|
export interface LogRowContextQueryErrors {
|
|
before?: string;
|
|
after?: string;
|
|
}
|
|
|
|
export interface HasMoreContextRows {
|
|
before: boolean;
|
|
after: boolean;
|
|
}
|
|
|
|
interface ResultType {
|
|
data: string[][];
|
|
errors: string[];
|
|
doNotCheckForMore?: boolean;
|
|
}
|
|
|
|
interface LogRowContextProviderProps {
|
|
row: LogRowModel;
|
|
logsSortOrder?: LogsSortOrder | null;
|
|
getRowContext: (row: LogRowModel, options?: RowContextOptions) => Promise<DataQueryResponse>;
|
|
children: (props: {
|
|
result: LogRowContextRows;
|
|
errors: LogRowContextQueryErrors;
|
|
hasMoreContextRows: HasMoreContextRows;
|
|
updateLimit: () => void;
|
|
runContextQuery: () => void;
|
|
limit: number;
|
|
logsSortOrder?: LogsSortOrder | null;
|
|
}) => JSX.Element;
|
|
}
|
|
|
|
export const getRowContexts = async (
|
|
getRowContext: (row: LogRowModel, options?: RowContextOptions) => Promise<DataQueryResponse>,
|
|
row: LogRowModel,
|
|
limit: number,
|
|
logsSortOrder?: LogsSortOrder | null
|
|
): Promise<ResultType> => {
|
|
const promises = [
|
|
getRowContext(row, {
|
|
limit,
|
|
}),
|
|
getRowContext(row, {
|
|
// The start time is inclusive so we will get the one row we are using as context entry
|
|
limit: limit + 1,
|
|
direction: 'FORWARD',
|
|
}),
|
|
];
|
|
|
|
const results: Array<DataQueryResponse | DataQueryError> = await Promise.all(promises.map((p) => p.catch((e) => e)));
|
|
|
|
const data = results.map((result) => {
|
|
const dataResult: DataQueryResponse = result as DataQueryResponse;
|
|
if (!dataResult.data) {
|
|
return [];
|
|
}
|
|
|
|
const data = [];
|
|
for (let index = 0; index < dataResult.data.length; index++) {
|
|
const dataFrame = toDataFrame(dataResult.data[index]);
|
|
const fieldCache = new FieldCache(dataFrame);
|
|
const timestampField: Field<string> = fieldCache.getFieldByName('ts')!;
|
|
const idField: Field<string> | undefined = fieldCache.getFieldByName('id');
|
|
|
|
for (let fieldIndex = 0; fieldIndex < timestampField.values.length; fieldIndex++) {
|
|
// TODO: this filtering is datasource dependant so it will make sense to move it there so the API is
|
|
// to return correct list of lines handling inclusive ranges or how to filter the correct line on the
|
|
// datasource.
|
|
|
|
// Filter out the row that is the one used as a focal point for the context as we will get it in one of the
|
|
// requests.
|
|
if (idField) {
|
|
// For Loki this means we filter only the one row. Issue is we could have other rows logged at the same
|
|
// ns which came before but they come in the response that search for logs after. This means right now
|
|
// we will show those as if they came after. This is not strictly correct but seems better than losing them
|
|
// and making this correct would mean quite a bit of complexity to shuffle things around and messing up
|
|
// counts.
|
|
if (idField.values.get(fieldIndex) === row.uid) {
|
|
continue;
|
|
}
|
|
} else {
|
|
// Fallback to timestamp. This should not happen right now as this feature is implemented only for loki
|
|
// and that has ID. Later this branch could be used in other DS but mind that this could also filter out
|
|
// logs which were logged in the same timestamp and that can be a problem depending on the precision.
|
|
if (parseInt(timestampField.values.get(fieldIndex), 10) === row.timeEpochMs) {
|
|
continue;
|
|
}
|
|
}
|
|
|
|
const lineField: Field<string> = dataFrame.fields.filter((field) => field.name === 'line')[0];
|
|
const line = lineField.values.get(fieldIndex); // assuming that both fields have same length
|
|
|
|
// since we request limit+1 logs, we occasionally get one extra log in the response
|
|
if (data.length < limit) {
|
|
data.push(line);
|
|
}
|
|
}
|
|
}
|
|
|
|
return logsSortOrder === LogsSortOrder.Ascending ? data.reverse() : data;
|
|
});
|
|
|
|
const errors = results.map((result) => {
|
|
const errorResult: DataQueryError = result as DataQueryError;
|
|
if (!errorResult.message) {
|
|
return '';
|
|
}
|
|
|
|
return errorResult.message;
|
|
});
|
|
|
|
return {
|
|
data: logsSortOrder === LogsSortOrder.Ascending ? data.reverse() : data,
|
|
errors: logsSortOrder === LogsSortOrder.Ascending ? errors.reverse() : errors,
|
|
};
|
|
};
|
|
|
|
export const LogRowContextProvider = ({ getRowContext, row, children, logsSortOrder }: LogRowContextProviderProps) => {
|
|
// React Hook that creates a number state value called limit to component state and a setter function called setLimit
|
|
// The initial value for limit is 10
|
|
// Used for the number of rows to retrieve from backend from a specific point in time
|
|
const [limit, setLimit] = useState(10);
|
|
|
|
// React Hook that creates an object state value called result to component state and a setter function called setResult
|
|
// The initial value for result is null
|
|
// Used for sorting the response from backend
|
|
const [result, setResult] = useState<ResultType>(null as unknown as ResultType);
|
|
|
|
// React Hook that creates an object state value called hasMoreContextRows to component state and a setter function called setHasMoreContextRows
|
|
// The initial value for hasMoreContextRows is {before: true, after: true}
|
|
// Used for indicating in UI if there are more rows to load in a given direction
|
|
const [hasMoreContextRows, setHasMoreContextRows] = useState({
|
|
before: true,
|
|
after: true,
|
|
});
|
|
|
|
const [results, setResults] = useState<ResultType>();
|
|
|
|
// React Hook that resolves two promises every time the limit prop changes
|
|
// First promise fetches limit number of rows backwards in time from a specific point in time
|
|
// Second promise fetches limit number of rows forwards in time from a specific point in time
|
|
const { value } = useAsync(async () => {
|
|
return await getRowContexts(getRowContext, row, limit, logsSortOrder); // Moved it to a separate function for debugging purposes
|
|
}, [limit]);
|
|
|
|
useEffect(() => {
|
|
setResults(value);
|
|
}, [value]);
|
|
|
|
// React Hook that performs a side effect every time the value (from useAsync hook) prop changes
|
|
// The side effect changes the result state with the response from the useAsync hook
|
|
// The side effect changes the hasMoreContextRows state if there are more context rows before or after the current result
|
|
useEffect(() => {
|
|
if (results) {
|
|
setResult((currentResult) => {
|
|
if (!results.doNotCheckForMore) {
|
|
let hasMoreLogsBefore = true,
|
|
hasMoreLogsAfter = true;
|
|
|
|
const currentResultBefore = currentResult?.data[0];
|
|
const currentResultAfter = currentResult?.data[1];
|
|
const valueBefore = results.data[0];
|
|
const valueAfter = results.data[1];
|
|
|
|
// checks if there are more log rows in a given direction
|
|
// if after fetching additional rows the length of result is the same,
|
|
// we can assume there are no logs in that direction within a given time range
|
|
if (currentResult && (!valueBefore || currentResultBefore.length === valueBefore.length)) {
|
|
hasMoreLogsBefore = false;
|
|
}
|
|
|
|
if (currentResult && (!valueAfter || currentResultAfter.length === valueAfter.length)) {
|
|
hasMoreLogsAfter = false;
|
|
}
|
|
|
|
setHasMoreContextRows({
|
|
before: hasMoreLogsBefore,
|
|
after: hasMoreLogsAfter,
|
|
});
|
|
}
|
|
|
|
return results;
|
|
});
|
|
}
|
|
}, [results]);
|
|
|
|
const updateLimit = useCallback(() => {
|
|
setLimit(limit + 10);
|
|
|
|
const { datasourceType, uid: logRowUid } = row;
|
|
reportInteraction('grafana_explore_logs_log_context_load_more_clicked', {
|
|
datasourceType,
|
|
logRowUid,
|
|
newLimit: limit + 10,
|
|
});
|
|
}, [limit, row]);
|
|
|
|
const runContextQuery = useCallback(async () => {
|
|
const results = await getRowContexts(getRowContext, row, limit, logsSortOrder);
|
|
results.doNotCheckForMore = true;
|
|
setResults(results);
|
|
}, [getRowContext, limit, logsSortOrder, row]);
|
|
|
|
const resultData = useMemo(
|
|
() => ({
|
|
before: result ? result.data[0] : [],
|
|
after: result ? result.data[1] : [],
|
|
}),
|
|
[result]
|
|
);
|
|
|
|
const errorsData = useMemo(
|
|
() => ({
|
|
before: result ? result.errors[0] : undefined,
|
|
after: result ? result.errors[1] : undefined,
|
|
}),
|
|
[result]
|
|
);
|
|
|
|
return children({
|
|
result: resultData,
|
|
errors: errorsData,
|
|
hasMoreContextRows,
|
|
updateLimit,
|
|
runContextQuery,
|
|
limit,
|
|
logsSortOrder,
|
|
});
|
|
};
|