mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Explore: Update the way Loki retrieve log context (#17204)
* Move log's typings into grafana/ui * Update the way context is retrieved for Loki Major changes: 1. getLogRowContext expects row to be of LogRowModel type 2. getLogRowContext accepts generic options object, specific to a datasource of interest. limit option has been removed, and now it's a part of Loki's context query options (see below) 3. LogRowContextProvider performs two queries now. Before, it was Loki ds that performed queries in both directions when getLogRowContext. 4. Loki's getLogRowContext accepts options object of a type: interface LokiContextQueryOptions { direction?: 'BACKWARD' | 'FORWARD'; limit?: number; } This will enable querying in either direction independently and also slightly simplifies the way query errors are handled. LogRowContextProvider maps the results to a Loki specific context types, basically string[][], as raw log lines are displayed in first version.
This commit is contained in:
parent
8749574975
commit
a9c94ec93b
@ -155,6 +155,7 @@
|
||||
"storybook:build": "cd packages/grafana-ui && yarn storybook:build",
|
||||
"themes:generate": "ts-node --project ./scripts/cli/tsconfig.json ./scripts/cli/generateSassVariableFiles.ts",
|
||||
"prettier:check": "prettier --list-different \"**/*.{ts,tsx,scss}\"",
|
||||
"prettier:write": "prettier --list-different \"**/*.{ts,tsx,scss}\" --write",
|
||||
"gui:tslint": "tslint -c ./packages/grafana-ui/tslint.json --project ./packages/grafana-ui/tsconfig.json",
|
||||
"gui:build": "ts-node --project ./scripts/cli/tsconfig.json ./scripts/cli/index.ts gui:build",
|
||||
"gui:releasePrepare": "ts-node --project ./scripts/cli/tsconfig.json ./scripts/cli/index.ts gui:release",
|
||||
|
@ -3,6 +3,7 @@ import { TimeRange } from './time';
|
||||
import { PluginMeta, GrafanaPlugin } from './plugin';
|
||||
import { TableData, TimeSeries, SeriesData, LoadingState } from './data';
|
||||
import { PanelData } from './panel';
|
||||
import { LogRowModel } from './logs';
|
||||
|
||||
// NOTE: this seems more general than just DataSource
|
||||
export interface DataSourcePluginOptionsEditorProps<TOptions> {
|
||||
@ -187,7 +188,10 @@ export abstract class DataSourceApi<
|
||||
/**
|
||||
* Retrieve context for a given log row
|
||||
*/
|
||||
getLogRowContext?(row: any, limit?: number): Promise<DataQueryResponse>;
|
||||
getLogRowContext?: <TContextQueryOptions extends {}>(
|
||||
row: LogRowModel,
|
||||
options?: TContextQueryOptions
|
||||
) => Promise<DataQueryResponse>;
|
||||
|
||||
/**
|
||||
* Set after constructor call, as the data source instance is the most common thing to pass around
|
||||
@ -299,10 +303,6 @@ export interface DataQueryResponse {
|
||||
data: DataQueryResponseData[];
|
||||
}
|
||||
|
||||
export interface LogRowContextQueryResponse {
|
||||
data: Array<Array<string | DataQueryError>>;
|
||||
}
|
||||
|
||||
export interface DataQuery {
|
||||
/**
|
||||
* A - Z
|
||||
|
@ -1,3 +1,5 @@
|
||||
import { Labels, TimeSeries } from './data';
|
||||
|
||||
/**
|
||||
* Mapping of log level abbreviation to canonical log level.
|
||||
* Supported levels are reduce to limit color variation.
|
||||
@ -19,3 +21,85 @@ export enum LogLevel {
|
||||
trace = 'trace',
|
||||
unknown = 'unknown',
|
||||
}
|
||||
|
||||
export enum LogsMetaKind {
|
||||
Number,
|
||||
String,
|
||||
LabelsMap,
|
||||
}
|
||||
|
||||
export interface LogsMetaItem {
|
||||
label: string;
|
||||
value: string | number | Labels;
|
||||
kind: LogsMetaKind;
|
||||
}
|
||||
|
||||
export interface LogRowModel {
|
||||
duplicates?: number;
|
||||
entry: string;
|
||||
hasAnsi: boolean;
|
||||
labels: Labels;
|
||||
logLevel: LogLevel;
|
||||
raw: string;
|
||||
searchWords?: string[];
|
||||
timestamp: string; // ISO with nanosec precision
|
||||
timeFromNow: string;
|
||||
timeEpochMs: number;
|
||||
timeLocal: string;
|
||||
uniqueLabels?: Labels;
|
||||
}
|
||||
|
||||
export interface LogsModel {
|
||||
hasUniqueLabels: boolean;
|
||||
meta?: LogsMetaItem[];
|
||||
rows: LogRowModel[];
|
||||
series?: TimeSeries[];
|
||||
}
|
||||
|
||||
export interface LogSearchMatch {
|
||||
start: number;
|
||||
length: number;
|
||||
text: string;
|
||||
}
|
||||
|
||||
export interface LogLabelStatsModel {
|
||||
active?: boolean;
|
||||
count: number;
|
||||
proportion: number;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export enum LogsDedupStrategy {
|
||||
none = 'none',
|
||||
exact = 'exact',
|
||||
numbers = 'numbers',
|
||||
signature = 'signature',
|
||||
}
|
||||
|
||||
export interface LogsParser {
|
||||
/**
|
||||
* Value-agnostic matcher for a field label.
|
||||
* Used to filter rows, and first capture group contains the value.
|
||||
*/
|
||||
buildMatcher: (label: string) => RegExp;
|
||||
|
||||
/**
|
||||
* Returns all parsable substrings from a line, used for highlighting
|
||||
*/
|
||||
getFields: (line: string) => string[];
|
||||
|
||||
/**
|
||||
* Gets the label name from a parsable substring of a line
|
||||
*/
|
||||
getLabelFromField: (field: string) => string;
|
||||
|
||||
/**
|
||||
* Gets the label value from a parsable substring of a line
|
||||
*/
|
||||
getValueFromField: (field: string) => string;
|
||||
/**
|
||||
* Function to verify if this is a valid parser for the given line.
|
||||
* The parser accepts the line unless it returns undefined.
|
||||
*/
|
||||
test: (line: string) => any;
|
||||
}
|
||||
|
@ -13,6 +13,13 @@ import {
|
||||
toLegacyResponseData,
|
||||
FieldCache,
|
||||
FieldType,
|
||||
LogRowModel,
|
||||
LogsModel,
|
||||
LogsMetaItem,
|
||||
LogsMetaKind,
|
||||
LogsParser,
|
||||
LogLabelStatsModel,
|
||||
LogsDedupStrategy,
|
||||
} from '@grafana/ui';
|
||||
import { getThemeColor } from 'app/core/utils/colors';
|
||||
import { hasAnsiCodes } from 'app/core/utils/text';
|
||||
@ -28,95 +35,12 @@ export const LogLevelColor = {
|
||||
[LogLevel.unknown]: getThemeColor('#8e8e8e', '#dde4ed'),
|
||||
};
|
||||
|
||||
export interface LogSearchMatch {
|
||||
start: number;
|
||||
length: number;
|
||||
text: string;
|
||||
}
|
||||
|
||||
export interface LogRowModel {
|
||||
duplicates?: number;
|
||||
entry: string;
|
||||
hasAnsi: boolean;
|
||||
labels: Labels;
|
||||
logLevel: LogLevel;
|
||||
raw: string;
|
||||
searchWords?: string[];
|
||||
timestamp: string; // ISO with nanosec precision
|
||||
timeFromNow: string;
|
||||
timeEpochMs: number;
|
||||
timeLocal: string;
|
||||
uniqueLabels?: Labels;
|
||||
}
|
||||
|
||||
export interface LogLabelStatsModel {
|
||||
active?: boolean;
|
||||
count: number;
|
||||
proportion: number;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export enum LogsMetaKind {
|
||||
Number,
|
||||
String,
|
||||
LabelsMap,
|
||||
}
|
||||
|
||||
export interface LogsMetaItem {
|
||||
label: string;
|
||||
value: string | number | Labels;
|
||||
kind: LogsMetaKind;
|
||||
}
|
||||
|
||||
export interface LogsModel {
|
||||
hasUniqueLabels: boolean;
|
||||
meta?: LogsMetaItem[];
|
||||
rows: LogRowModel[];
|
||||
series?: TimeSeries[];
|
||||
}
|
||||
|
||||
export enum LogsDedupDescription {
|
||||
none = 'No de-duplication',
|
||||
exact = 'De-duplication of successive lines that are identical, ignoring ISO datetimes.',
|
||||
numbers = 'De-duplication of successive lines that are identical when ignoring numbers, e.g., IP addresses, latencies.',
|
||||
signature = 'De-duplication of successive lines that have identical punctuation and whitespace.',
|
||||
}
|
||||
|
||||
export enum LogsDedupStrategy {
|
||||
none = 'none',
|
||||
exact = 'exact',
|
||||
numbers = 'numbers',
|
||||
signature = 'signature',
|
||||
}
|
||||
|
||||
export interface LogsParser {
|
||||
/**
|
||||
* Value-agnostic matcher for a field label.
|
||||
* Used to filter rows, and first capture group contains the value.
|
||||
*/
|
||||
buildMatcher: (label: string) => RegExp;
|
||||
|
||||
/**
|
||||
* Returns all parsable substrings from a line, used for highlighting
|
||||
*/
|
||||
getFields: (line: string) => string[];
|
||||
|
||||
/**
|
||||
* Gets the label name from a parsable substring of a line
|
||||
*/
|
||||
getLabelFromField: (field: string) => string;
|
||||
|
||||
/**
|
||||
* Gets the label value from a parsable substring of a line
|
||||
*/
|
||||
getValueFromField: (field: string) => string;
|
||||
/**
|
||||
* Function to verify if this is a valid parser for the given line.
|
||||
* The parser accepts the line unless it returns undefined.
|
||||
*/
|
||||
test: (line: string) => any;
|
||||
}
|
||||
|
||||
const LOGFMT_REGEXP = /(?:^|\s)(\w+)=("[^"]*"|\S+)/;
|
||||
|
||||
export const LogsParsers: { [name: string]: LogsParser } = {
|
||||
|
@ -1,15 +1,12 @@
|
||||
import { SeriesData, FieldType, LogsModel, LogsMetaKind, LogsDedupStrategy } from '@grafana/ui';
|
||||
import {
|
||||
dedupLogRows,
|
||||
calculateFieldStats,
|
||||
calculateLogsLabelStats,
|
||||
dedupLogRows,
|
||||
getParser,
|
||||
LogsDedupStrategy,
|
||||
LogsModel,
|
||||
LogsParsers,
|
||||
seriesDataToLogsModel,
|
||||
LogsMetaKind,
|
||||
} from '../logs_model';
|
||||
import { SeriesData, FieldType } from '@grafana/ui';
|
||||
|
||||
describe('dedupLogRows()', () => {
|
||||
test('should return rows as is when dedup is set to none', () => {
|
||||
|
@ -12,8 +12,7 @@ import {
|
||||
} from './explore';
|
||||
import { ExploreUrlState } from 'app/types/explore';
|
||||
import store from 'app/core/store';
|
||||
import { LogsDedupStrategy } from 'app/core/logs_model';
|
||||
import { DataQueryError } from '@grafana/ui';
|
||||
import { DataQueryError, LogsDedupStrategy } from '@grafana/ui';
|
||||
|
||||
const DEFAULT_EXPLORE_STATE: ExploreUrlState = {
|
||||
datasource: null,
|
||||
|
@ -22,6 +22,9 @@ import {
|
||||
guessFieldTypes,
|
||||
TimeFragment,
|
||||
DataQueryError,
|
||||
LogRowModel,
|
||||
LogsModel,
|
||||
LogsDedupStrategy,
|
||||
} from '@grafana/ui';
|
||||
import TimeSeries from 'app/core/time_series2';
|
||||
import {
|
||||
@ -33,7 +36,7 @@ import {
|
||||
QueryOptions,
|
||||
ResultGetter,
|
||||
} from 'app/types/explore';
|
||||
import { LogsDedupStrategy, seriesDataToLogsModel, LogsModel, LogRowModel } from 'app/core/logs_model';
|
||||
import { seriesDataToLogsModel } from 'app/core/logs_model';
|
||||
import { toUtc } from '@grafana/ui/src/utils/moment_wrapper';
|
||||
import { isLive } from '@grafana/ui/src/components/RefreshPicker/RefreshPicker';
|
||||
|
||||
|
@ -1,8 +1,15 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import { css, cx } from 'emotion';
|
||||
import { Themeable, withTheme, GrafanaTheme, selectThemeVariant, LinkButton } from '@grafana/ui';
|
||||
import {
|
||||
Themeable,
|
||||
withTheme,
|
||||
GrafanaTheme,
|
||||
selectThemeVariant,
|
||||
LinkButton,
|
||||
LogsModel,
|
||||
LogRowModel,
|
||||
} from '@grafana/ui';
|
||||
|
||||
import { LogsModel, LogRowModel } from 'app/core/logs_model';
|
||||
import ElapsedTime from './ElapsedTime';
|
||||
import { ButtonSize, ButtonVariant } from '@grafana/ui/src/components/Button/AbstractButton';
|
||||
|
||||
|
@ -1,7 +1,8 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
|
||||
import { calculateLogsLabelStats, LogLabelStatsModel, LogRowModel } from 'app/core/logs_model';
|
||||
import { LogLabelStats } from './LogLabelStats';
|
||||
import { LogRowModel, LogLabelStatsModel } from '@grafana/ui';
|
||||
import { calculateLogsLabelStats } from 'app/core/logs_model';
|
||||
|
||||
interface Props {
|
||||
getRows?: () => LogRowModel[];
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import classnames from 'classnames';
|
||||
import { LogLabelStatsModel } from 'app/core/logs_model';
|
||||
import { LogLabelStatsModel } from '@grafana/ui';
|
||||
|
||||
function LogLabelStatsRow(logLabelStatsModel: LogLabelStatsModel) {
|
||||
const { active, count, proportion, value } = logLabelStatsModel;
|
||||
|
@ -1,8 +1,7 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
|
||||
import { LogRowModel } from 'app/core/logs_model';
|
||||
import { LogLabel } from './LogLabel';
|
||||
import { Labels } from '@grafana/ui';
|
||||
import { Labels, LogRowModel } from '@grafana/ui';
|
||||
|
||||
interface Props {
|
||||
getRows?: () => LogRowModel[];
|
||||
|
@ -3,7 +3,7 @@ import _ from 'lodash';
|
||||
import Highlighter from 'react-highlight-words';
|
||||
import classnames from 'classnames';
|
||||
|
||||
import { LogRowModel, LogLabelStatsModel, LogsParser, calculateFieldStats, getParser } from 'app/core/logs_model';
|
||||
import { calculateFieldStats, getParser } from 'app/core/logs_model';
|
||||
import { LogLabels } from './LogLabels';
|
||||
import { findHighlightChunksInText } from 'app/core/utils/text';
|
||||
import { LogLabelStats } from './LogLabelStats';
|
||||
@ -15,7 +15,15 @@ import {
|
||||
HasMoreContextRows,
|
||||
LogRowContextQueryErrors,
|
||||
} from './LogRowContextProvider';
|
||||
import { ThemeContext, selectThemeVariant, GrafanaTheme, DataQueryResponse } from '@grafana/ui';
|
||||
import {
|
||||
ThemeContext,
|
||||
selectThemeVariant,
|
||||
GrafanaTheme,
|
||||
DataQueryResponse,
|
||||
LogRowModel,
|
||||
LogLabelStatsModel,
|
||||
LogsParser,
|
||||
} from '@grafana/ui';
|
||||
import { LogRowContext } from './LogRowContext';
|
||||
import tinycolor from 'tinycolor2';
|
||||
|
||||
@ -29,7 +37,7 @@ interface Props {
|
||||
getRows: () => LogRowModel[];
|
||||
onClickLabel?: (label: string, value: string) => void;
|
||||
onContextClick?: () => void;
|
||||
getRowContext?: (row: LogRowModel, limit: number) => Promise<DataQueryResponse>;
|
||||
getRowContext?: (row: LogRowModel, options?: any) => Promise<DataQueryResponse>;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
|
@ -7,10 +7,10 @@ import {
|
||||
ClickOutsideWrapper,
|
||||
CustomScrollbar,
|
||||
DataQueryError,
|
||||
LogRowModel,
|
||||
} from '@grafana/ui';
|
||||
import { css, cx } from 'emotion';
|
||||
import { LogRowContextRows, HasMoreContextRows, LogRowContextQueryErrors } from './LogRowContextProvider';
|
||||
import { LogRowModel } from 'app/core/logs_model';
|
||||
import { Alert } from './Error';
|
||||
|
||||
interface LogRowContextProps {
|
||||
|
@ -1,11 +1,11 @@
|
||||
import { LogRowModel } from 'app/core/logs_model';
|
||||
import { LogRowContextQueryResponse, SeriesData, DataQueryResponse, DataQueryError } from '@grafana/ui';
|
||||
import { DataQueryResponse, DataQueryError, LogRowModel } from '@grafana/ui';
|
||||
import { useState, useEffect } from 'react';
|
||||
import flatten from 'lodash/flatten';
|
||||
import useAsync from 'react-use/lib/useAsync';
|
||||
|
||||
export interface LogRowContextRows {
|
||||
before?: Array<string | DataQueryError>;
|
||||
after?: Array<string | DataQueryError>;
|
||||
before?: string[];
|
||||
after?: string[];
|
||||
}
|
||||
export interface LogRowContextQueryErrors {
|
||||
before?: string;
|
||||
@ -19,7 +19,7 @@ export interface HasMoreContextRows {
|
||||
|
||||
interface LogRowContextProviderProps {
|
||||
row: LogRowModel;
|
||||
getRowContext: (row: LogRowModel, limit: number) => Promise<DataQueryResponse>;
|
||||
getRowContext: (row: LogRowModel, options?: any) => Promise<DataQueryResponse>;
|
||||
children: (props: {
|
||||
result: LogRowContextRows;
|
||||
errors: LogRowContextQueryErrors;
|
||||
@ -34,23 +34,44 @@ export const LogRowContextProvider: React.FunctionComponent<LogRowContextProvide
|
||||
children,
|
||||
}) => {
|
||||
const [limit, setLimit] = useState(10);
|
||||
const [result, setResult] = useState<LogRowContextQueryResponse>(null);
|
||||
const [errors, setErrors] = useState<LogRowContextQueryErrors>(null);
|
||||
const [result, setResult] = useState<{
|
||||
data: string[][];
|
||||
errors: string[];
|
||||
}>(null);
|
||||
const [hasMoreContextRows, setHasMoreContextRows] = useState({
|
||||
before: true,
|
||||
after: true,
|
||||
});
|
||||
|
||||
const { value } = useAsync(async () => {
|
||||
const context = await getRowContext(row, limit);
|
||||
const promises = [
|
||||
getRowContext(row, {
|
||||
limit,
|
||||
}),
|
||||
getRowContext(row, {
|
||||
limit,
|
||||
direction: 'FORWARD',
|
||||
}),
|
||||
];
|
||||
|
||||
const results: Array<DataQueryResponse | DataQueryError> = await Promise.all(promises.map(p => p.catch(e => e)));
|
||||
|
||||
return {
|
||||
data: context.data.map(series => {
|
||||
if ((series as SeriesData).rows) {
|
||||
return (series as SeriesData).rows.map(row => row[1]);
|
||||
data: results.map(result => {
|
||||
if ((result as DataQueryResponse).data) {
|
||||
return (result as DataQueryResponse).data.map(series => {
|
||||
return series.rows.map(row => row[1]);
|
||||
});
|
||||
} else {
|
||||
return [series];
|
||||
return [];
|
||||
}
|
||||
}),
|
||||
errors: results.map(result => {
|
||||
if ((result as DataQueryError).message) {
|
||||
return (result as DataQueryError).message;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
return [];
|
||||
}),
|
||||
};
|
||||
}, [limit]);
|
||||
@ -60,7 +81,6 @@ export const LogRowContextProvider: React.FunctionComponent<LogRowContextProvide
|
||||
setResult(currentResult => {
|
||||
let hasMoreLogsBefore = true,
|
||||
hasMoreLogsAfter = true;
|
||||
let beforeContextError, afterContextError;
|
||||
|
||||
if (currentResult && currentResult.data[0].length === value.data[0].length) {
|
||||
hasMoreLogsBefore = false;
|
||||
@ -70,23 +90,11 @@ export const LogRowContextProvider: React.FunctionComponent<LogRowContextProvide
|
||||
hasMoreLogsAfter = false;
|
||||
}
|
||||
|
||||
if (value.data[0] && value.data[0].length > 0 && value.data[0][0].message) {
|
||||
beforeContextError = value.data[0][0].message;
|
||||
}
|
||||
if (value.data[1] && value.data[1].length > 0 && value.data[1][0].message) {
|
||||
afterContextError = value.data[1][0].message;
|
||||
}
|
||||
|
||||
setHasMoreContextRows({
|
||||
before: hasMoreLogsBefore,
|
||||
after: hasMoreLogsAfter,
|
||||
});
|
||||
|
||||
setErrors({
|
||||
before: beforeContextError,
|
||||
after: afterContextError,
|
||||
});
|
||||
|
||||
return value;
|
||||
});
|
||||
}
|
||||
@ -94,10 +102,13 @@ export const LogRowContextProvider: React.FunctionComponent<LogRowContextProvide
|
||||
|
||||
return children({
|
||||
result: {
|
||||
before: result ? result.data[0] : [],
|
||||
after: result ? result.data[1] : [],
|
||||
before: result ? flatten(result.data[0]) : [],
|
||||
after: result ? flatten(result.data[1]) : [],
|
||||
},
|
||||
errors: {
|
||||
before: result ? result.errors[0] : null,
|
||||
after: result ? result.errors[1] : null,
|
||||
},
|
||||
errors,
|
||||
hasMoreContextRows,
|
||||
updateLimit: () => setLimit(limit + 10),
|
||||
});
|
||||
|
@ -2,16 +2,26 @@ import _ from 'lodash';
|
||||
import React, { PureComponent } from 'react';
|
||||
|
||||
import * as rangeUtil from '@grafana/ui/src/utils/rangeutil';
|
||||
import { RawTimeRange, Switch, LogLevel, TimeZone, TimeRange, AbsoluteTimeRange } from '@grafana/ui';
|
||||
import {
|
||||
RawTimeRange,
|
||||
Switch,
|
||||
LogLevel,
|
||||
TimeZone,
|
||||
TimeRange,
|
||||
AbsoluteTimeRange,
|
||||
LogsMetaKind,
|
||||
LogsModel,
|
||||
LogsDedupStrategy,
|
||||
LogRowModel,
|
||||
} from '@grafana/ui';
|
||||
import TimeSeries from 'app/core/time_series2';
|
||||
|
||||
import { LogsDedupDescription, LogsDedupStrategy, LogsModel, LogsMetaKind, LogRowModel } from 'app/core/logs_model';
|
||||
|
||||
import ToggleButtonGroup, { ToggleButton } from 'app/core/components/ToggleButtonGroup/ToggleButtonGroup';
|
||||
|
||||
import Graph from './Graph';
|
||||
import { LogLabels } from './LogLabels';
|
||||
import { LogRow } from './LogRow';
|
||||
import { LogsDedupDescription } from 'app/core/logs_model';
|
||||
|
||||
const PREVIEW_LIMIT = 100;
|
||||
|
||||
@ -60,7 +70,7 @@ interface Props {
|
||||
onStopScanning?: () => void;
|
||||
onDedupStrategyChange: (dedupStrategy: LogsDedupStrategy) => void;
|
||||
onToggleLogLevel: (hiddenLogLevels: Set<LogLevel>) => void;
|
||||
getRowContext?: (row: LogRowModel, limit: number) => Promise<any>;
|
||||
getRowContext?: (row: LogRowModel, options?: any) => Promise<any>;
|
||||
}
|
||||
|
||||
interface State {
|
||||
|
@ -10,10 +10,12 @@ import {
|
||||
toUtc,
|
||||
dateTime,
|
||||
DataSourceApi,
|
||||
LogsModel,
|
||||
LogRowModel,
|
||||
LogsDedupStrategy,
|
||||
} from '@grafana/ui';
|
||||
|
||||
import { ExploreId, ExploreItemState } from 'app/types/explore';
|
||||
import { LogsModel, LogsDedupStrategy, LogRowModel } from 'app/core/logs_model';
|
||||
import { StoreState } from 'app/types';
|
||||
|
||||
import { changeDedupStrategy, changeTime } from './state/actions';
|
||||
@ -29,6 +31,7 @@ interface LogsContainerProps {
|
||||
datasourceInstance: DataSourceApi | null;
|
||||
exploreId: ExploreId;
|
||||
loading: boolean;
|
||||
|
||||
logsHighlighterExpressions?: string[];
|
||||
logsResult?: LogsModel;
|
||||
dedupedResult?: LogsModel;
|
||||
@ -77,11 +80,11 @@ export class LogsContainer extends PureComponent<LogsContainerProps> {
|
||||
});
|
||||
};
|
||||
|
||||
getLogRowContext = async (row: LogRowModel, limit: number) => {
|
||||
getLogRowContext = async (row: LogRowModel, options?: any) => {
|
||||
const { datasourceInstance } = this.props;
|
||||
|
||||
if (datasourceInstance) {
|
||||
return datasourceInstance.getLogRowContext(row, limit);
|
||||
return datasourceInstance.getLogRowContext(row, options);
|
||||
}
|
||||
|
||||
return [];
|
||||
|
@ -1,7 +1,6 @@
|
||||
import { refreshExplore, testDatasource, loadDatasource } from './actions';
|
||||
import { ExploreId, ExploreUrlState, ExploreUpdateState } from 'app/types';
|
||||
import { thunkTester } from 'test/core/thunk/thunkTester';
|
||||
import { LogsDedupStrategy } from 'app/core/logs_model';
|
||||
import {
|
||||
initializeExploreAction,
|
||||
InitializeExplorePayload,
|
||||
@ -18,7 +17,7 @@ import { Emitter } from 'app/core/core';
|
||||
import { ActionOf } from 'app/core/redux/actionCreatorFactory';
|
||||
import { makeInitialUpdateState } from './reducers';
|
||||
import { DataQuery } from '@grafana/ui/src/types/datasource';
|
||||
import { DefaultTimeZone, RawTimeRange } from '@grafana/ui';
|
||||
import { DefaultTimeZone, RawTimeRange, LogsDedupStrategy } from '@grafana/ui';
|
||||
import { toUtc } from '@grafana/ui/src/utils/moment_wrapper';
|
||||
|
||||
jest.mock('app/features/plugins/datasource_srv', () => ({
|
||||
|
@ -35,7 +35,8 @@ import {
|
||||
DataSourceSelectItem,
|
||||
QueryFixAction,
|
||||
TimeRange,
|
||||
} from '@grafana/ui/src/types';
|
||||
LogsDedupStrategy,
|
||||
} from '@grafana/ui';
|
||||
import {
|
||||
ExploreId,
|
||||
ExploreUrlState,
|
||||
@ -87,7 +88,6 @@ import {
|
||||
changeModeAction,
|
||||
} from './actionTypes';
|
||||
import { ActionOf, ActionCreator } from 'app/core/redux/actionCreatorFactory';
|
||||
import { LogsDedupStrategy } from 'app/core/logs_model';
|
||||
import { getTimeZone } from 'app/features/profile/state/selectors';
|
||||
import { isDateTime } from '@grafana/ui/src/utils/moment_wrapper';
|
||||
import { toDataQueryError } from 'app/features/dashboard/state/PanelQueryState';
|
||||
|
@ -29,10 +29,9 @@ import {
|
||||
import { Reducer } from 'redux';
|
||||
import { ActionOf } from 'app/core/redux/actionCreatorFactory';
|
||||
import { updateLocation } from 'app/core/actions/location';
|
||||
import { LogsDedupStrategy, LogsModel } from 'app/core/logs_model';
|
||||
import { serializeStateToUrlParam } from 'app/core/utils/explore';
|
||||
import TableModel from 'app/core/table_model';
|
||||
import { DataSourceApi, DataQuery } from '@grafana/ui';
|
||||
import { DataSourceApi, DataQuery, LogsModel, LogsDedupStrategy } from '@grafana/ui';
|
||||
|
||||
describe('Explore item reducer', () => {
|
||||
describe('scanning', () => {
|
||||
|
@ -10,7 +10,7 @@ import {
|
||||
sortLogsResult,
|
||||
} from 'app/core/utils/explore';
|
||||
import { ExploreItemState, ExploreState, ExploreId, ExploreUpdateState, ExploreMode } from 'app/types/explore';
|
||||
import { DataQuery } from '@grafana/ui/src/types';
|
||||
import { DataQuery, LogsModel } from '@grafana/ui';
|
||||
import {
|
||||
HigherOrderAction,
|
||||
ActionTypes,
|
||||
@ -58,7 +58,7 @@ import { LocationUpdate } from 'app/types';
|
||||
import TableModel from 'app/core/table_model';
|
||||
import { isLive } from '@grafana/ui/src/components/RefreshPicker/RefreshPicker';
|
||||
import { subscriptionDataReceivedAction, startSubscriptionAction } from './epics';
|
||||
import { LogsModel, seriesDataToLogsModel } from 'app/core/logs_model';
|
||||
import { seriesDataToLogsModel } from 'app/core/logs_model';
|
||||
|
||||
export const DEFAULT_RANGE = {
|
||||
from: 'now-6h',
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { deduplicatedLogsSelector } from './selectors';
|
||||
import { LogsDedupStrategy } from 'app/core/logs_model';
|
||||
import { LogsDedupStrategy } from '@grafana/ui';
|
||||
import { ExploreItemState } from 'app/types';
|
||||
|
||||
const state = {
|
||||
|
@ -16,12 +16,12 @@ import {
|
||||
DataSourceApi,
|
||||
DataSourceInstanceSettings,
|
||||
DataQueryError,
|
||||
} from '@grafana/ui/src/types';
|
||||
LogRowModel,
|
||||
} from '@grafana/ui';
|
||||
import { LokiQuery, LokiOptions } from './types';
|
||||
import { BackendSrv } from 'app/core/services/backend_srv';
|
||||
import { TemplateSrv } from 'app/features/templating/template_srv';
|
||||
import { safeStringifyValue } from 'app/core/utils/explore';
|
||||
import { LogRowModel } from 'app/core/logs_model';
|
||||
|
||||
export const DEFAULT_MAX_LINES = 1000;
|
||||
|
||||
@ -41,6 +41,11 @@ function serializeParams(data: any) {
|
||||
.join('&');
|
||||
}
|
||||
|
||||
interface LokiContextQueryOptions {
|
||||
direction?: 'BACKWARD' | 'FORWARD';
|
||||
limit?: number;
|
||||
}
|
||||
|
||||
export class LokiDatasource extends DataSourceApi<LokiQuery, LokiOptions> {
|
||||
languageProvider: LanguageProvider;
|
||||
maxLines: number;
|
||||
@ -224,7 +229,7 @@ export class LokiDatasource extends DataSourceApi<LokiQuery, LokiOptions> {
|
||||
return Math.ceil(date.valueOf() * 1e6);
|
||||
}
|
||||
|
||||
prepareLogRowContextQueryTargets = (row: LogRowModel, limit: number) => {
|
||||
prepareLogRowContextQueryTarget = (row: LogRowModel, limit: number, direction: 'BACKWARD' | 'FORWARD') => {
|
||||
const query = Object.keys(row.labels)
|
||||
.map(label => {
|
||||
return `${label}="${row.labels[label]}"`;
|
||||
@ -236,69 +241,58 @@ export class LokiDatasource extends DataSourceApi<LokiQuery, LokiOptions> {
|
||||
const commontTargetOptons = {
|
||||
limit,
|
||||
query: `{${query}}`,
|
||||
direction,
|
||||
};
|
||||
return [
|
||||
// Target for "before" context
|
||||
{
|
||||
|
||||
if (direction === 'BACKWARD') {
|
||||
return {
|
||||
...commontTargetOptons,
|
||||
start: timeEpochNs - contextTimeBuffer,
|
||||
end: timeEpochNs,
|
||||
direction: 'BACKWARD',
|
||||
},
|
||||
// Target for "after" context
|
||||
{
|
||||
direction,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
...commontTargetOptons,
|
||||
start: timeEpochNs, // TODO: We should add 1ns here for the original row not no be included in the result
|
||||
end: timeEpochNs + contextTimeBuffer,
|
||||
direction: 'FORWARD',
|
||||
},
|
||||
];
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
getLogRowContext = (row: LogRowModel, limit?: number) => {
|
||||
// Preparing two targets, for preceeding and following log queries
|
||||
const targets = this.prepareLogRowContextQueryTargets(row, limit || 10);
|
||||
getLogRowContext = async (row: LogRowModel, options?: LokiContextQueryOptions) => {
|
||||
const target = this.prepareLogRowContextQueryTarget(
|
||||
row,
|
||||
(options && options.limit) || 10,
|
||||
(options && options.direction) || 'BACKWARD'
|
||||
);
|
||||
const series: SeriesData[] = [];
|
||||
|
||||
return Promise.all(
|
||||
targets.map(target => {
|
||||
return this._request('/api/prom/query', target).catch(e => {
|
||||
const error: DataQueryError = {
|
||||
message: 'Error during context query. Please check JS console logs.',
|
||||
status: e.status,
|
||||
statusText: e.statusText,
|
||||
};
|
||||
return error;
|
||||
});
|
||||
})
|
||||
).then((results: any[]) => {
|
||||
const series: Array<Array<SeriesData | DataQueryError>> = [];
|
||||
const emptySeries = {
|
||||
fields: [],
|
||||
rows: [],
|
||||
} as SeriesData;
|
||||
|
||||
for (let i = 0; i < results.length; i++) {
|
||||
const result = results[i];
|
||||
series[i] = [];
|
||||
if (result.data) {
|
||||
for (const stream of result.data.streams || []) {
|
||||
const seriesData = logStreamToSeriesData(stream);
|
||||
series[i].push(seriesData);
|
||||
}
|
||||
} else {
|
||||
series[i].push(result);
|
||||
try {
|
||||
const result = await this._request('/api/prom/query', target);
|
||||
if (result.data) {
|
||||
for (const stream of result.data.streams || []) {
|
||||
const seriesData = logStreamToSeriesData(stream);
|
||||
series.push(seriesData);
|
||||
}
|
||||
}
|
||||
if (options && options.direction === 'FORWARD') {
|
||||
if (series[0] && series[0].rows) {
|
||||
series[0].rows.reverse();
|
||||
}
|
||||
}
|
||||
|
||||
// Following context logs are requested in "forward" direction.
|
||||
// This means, that we need to reverse those to make them sorted
|
||||
// in descending order (by timestamp)
|
||||
if (series[1][0] && (series[1][0] as SeriesData).rows) {
|
||||
(series[1][0] as SeriesData).rows.reverse();
|
||||
}
|
||||
|
||||
return { data: [series[0][0] || emptySeries, series[1][0] || emptySeries] };
|
||||
});
|
||||
return {
|
||||
data: series,
|
||||
};
|
||||
} catch (e) {
|
||||
const error: DataQueryError = {
|
||||
message: 'Error during context query. Please check JS console logs.',
|
||||
status: e.status,
|
||||
statusText: e.statusText,
|
||||
};
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
testDatasource() {
|
||||
|
@ -11,10 +11,11 @@ import {
|
||||
LogLevel,
|
||||
TimeRange,
|
||||
DataQueryError,
|
||||
LogsModel,
|
||||
LogsDedupStrategy,
|
||||
} from '@grafana/ui';
|
||||
|
||||
import { Emitter, TimeSeries } from 'app/core/core';
|
||||
import { LogsModel, LogsDedupStrategy } from 'app/core/logs_model';
|
||||
import TableModel from 'app/core/table_model';
|
||||
|
||||
export enum ExploreMode {
|
||||
|
Loading…
Reference in New Issue
Block a user