Explore: display log line context (#17097)

* Extend DataSourceAPI to enable log row context retrieval

* Add react-use package

* Display log row context in UI

* Make Loki datasource return "after" log context in correct order

* Don't show Load more context links  when there are no more new results

* Update getLogRowContext to return DataQueryResponse

* Use DataQueryResponse in log row context provider, filter out original row  being duplicated in context
This commit is contained in:
Dominik Prokop 2019-05-20 08:44:37 +02:00 committed by GitHub
parent ae1df1cf89
commit 12e0616413
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 785 additions and 63 deletions

View File

@ -224,6 +224,7 @@
"react-sizeme": "2.5.2",
"react-table": "6.9.2",
"react-transition-group": "2.6.1",
"react-use": "9.0.0",
"react-virtualized": "9.21.0",
"react-window": "1.7.1",
"redux": "4.0.1",

View File

@ -172,6 +172,11 @@ export abstract class DataSourceApi<
*/
getQueryDisplayText?(query: TQuery): string;
/**
* Retrieve context for a given log row
*/
getLogRowContext?(row: any, limit?: number): Promise<DataQueryResponse>;
/**
* Set after constructor call, as the data source instance is the most common thing to pass around
* we attach the components to this instance for easy access
@ -282,6 +287,10 @@ export interface DataQueryResponse {
data: DataQueryResponseData[];
}
export interface LogRowContextQueryResponse {
data: Array<Array<string | DataQueryError>>;
}
export interface DataQuery {
/**
* A - Z

View File

@ -8,6 +8,16 @@ import { LogLabels } from './LogLabels';
import { findHighlightChunksInText } from 'app/core/utils/text';
import { LogLabelStats } from './LogLabelStats';
import { LogMessageAnsi } from './LogMessageAnsi';
import { css, cx } from 'emotion';
import {
LogRowContextProvider,
LogRowContextRows,
HasMoreContextRows,
LogRowContextQueryErrors,
} from './LogRowContextProvider';
import { ThemeContext, selectThemeVariant, GrafanaTheme, DataQueryResponse } from '@grafana/ui';
import { LogRowContext } from './LogRowContext';
import tinycolor from 'tinycolor2';
interface Props {
highlighterExpressions?: string[];
@ -18,6 +28,9 @@ interface Props {
showUtc: boolean;
getRows: () => LogRowModel[];
onClickLabel?: (label: string, value: string) => void;
onContextClick?: () => void;
getRowContext?: (row: LogRowModel, limit: number) => Promise<DataQueryResponse>;
className?: string;
}
interface State {
@ -29,6 +42,7 @@ interface State {
parser?: LogsParser;
parsedFieldHighlights: string[];
showFieldStats: boolean;
showContext: boolean;
}
/**
@ -44,6 +58,32 @@ const FieldHighlight = onClick => props => {
);
};
const logRowStyles = css`
position: relative;
/* z-index: 0; */
/* outline: none; */
`;
const getLogRowWithContextStyles = (theme: GrafanaTheme, state: State) => {
const outlineColor = selectThemeVariant(
{
light: theme.colors.white,
dark: theme.colors.black,
},
theme.type
);
return {
row: css`
z-index: 1;
outline: 9999px solid
${tinycolor(outlineColor)
.setAlpha(0.7)
.toRgbString()};
`,
};
};
/**
* Renders a log line.
*
@ -63,6 +103,7 @@ export class LogRow extends PureComponent<Props, State> {
parser: undefined,
parsedFieldHighlights: [],
showFieldStats: false,
showContext: false,
};
componentWillUnmount() {
@ -89,11 +130,21 @@ export class LogRow extends PureComponent<Props, State> {
};
onMouseOverMessage = () => {
if (this.state.showContext) {
// When showing context we don't want to the LogRow rerender as it will mess up state of context block
// making the "after" context to be scrolled to the top, what is desired only on open
// The log row message needs to be refactored to separate component that encapsulates parsing and parsed message state
return;
}
// Don't parse right away, user might move along
this.mouseMessageTimer = setTimeout(this.parseMessage, 500);
};
onMouseOutMessage = () => {
if (this.state.showContext) {
// See comment in onMouseOverMessage method
return;
}
clearTimeout(this.mouseMessageTimer);
this.setState({ parsed: false });
};
@ -110,7 +161,25 @@ export class LogRow extends PureComponent<Props, State> {
}
};
render() {
toggleContext = () => {
this.setState(state => {
return {
showContext: !state.showContext,
};
});
};
onContextToggle = (e: React.SyntheticEvent<HTMLElement>) => {
e.stopPropagation();
this.toggleContext();
};
renderLogRow(
context?: LogRowContextRows,
errors?: LogRowContextQueryErrors,
hasMoreContextRows?: HasMoreContextRows,
updateLimit?: () => void
) {
const {
getRows,
highlighterExpressions,
@ -129,6 +198,7 @@ export class LogRow extends PureComponent<Props, State> {
parsed,
parsedFieldHighlights,
showFieldStats,
showContext,
} = this.state;
const { entry, hasAnsi, raw } = row;
const previewHighlights = highlighterExpressions && !_.isEqual(highlighterExpressions, row.searchWords);
@ -139,59 +209,132 @@ export class LogRow extends PureComponent<Props, State> {
});
return (
<div className="logs-row">
{showDuplicates && (
<div className="logs-row__duplicates">{row.duplicates > 0 ? `${row.duplicates + 1}x` : null}</div>
)}
<div className={row.logLevel ? `logs-row__level logs-row__level--${row.logLevel}` : ''} />
{showUtc && (
<div className="logs-row__time" title={`Local: ${row.timeLocal} (${row.timeFromNow})`}>
{row.timestamp}
</div>
)}
{showLocalTime && (
<div className="logs-row__localtime" title={`${row.timestamp} (${row.timeFromNow})`}>
{row.timeLocal}
</div>
)}
{showLabels && (
<div className="logs-row__labels">
<LogLabels getRows={getRows} labels={row.uniqueLabels} onClickLabel={onClickLabel} />
</div>
)}
<div className="logs-row__message" onMouseEnter={this.onMouseOverMessage} onMouseLeave={this.onMouseOutMessage}>
{parsed && (
<Highlighter
autoEscape
highlightTag={FieldHighlight(this.onClickHighlight)}
textToHighlight={entry}
searchWords={parsedFieldHighlights}
highlightClassName="logs-row__field-highlight"
/>
)}
{!parsed && needsHighlighter && (
<Highlighter
textToHighlight={entry}
searchWords={highlights}
findChunks={findHighlightChunksInText}
highlightClassName={highlightClassName}
/>
)}
{hasAnsi && !parsed && !needsHighlighter && <LogMessageAnsi value={raw} />}
{!hasAnsi && !parsed && !needsHighlighter && entry}
{showFieldStats && (
<div className="logs-row__stats">
<LogLabelStats
stats={fieldStats}
label={fieldLabel}
value={fieldValue}
onClickClose={this.onClickClose}
rowCount={fieldCount}
/>
<ThemeContext.Consumer>
{theme => {
const styles = this.state.showContext
? cx(logRowStyles, getLogRowWithContextStyles(theme, this.state).row)
: logRowStyles;
console.log(styles);
return (
<div className={`logs-row ${this.props.className}`}>
{showDuplicates && (
<div className="logs-row__duplicates">{row.duplicates > 0 ? `${row.duplicates + 1}x` : null}</div>
)}
<div className={row.logLevel ? `logs-row__level logs-row__level--${row.logLevel}` : ''} />
{showUtc && (
<div className="logs-row__time" title={`Local: ${row.timeLocal} (${row.timeFromNow})`}>
{row.timestamp}
</div>
)}
{showLocalTime && (
<div className="logs-row__localtime" title={`${row.timestamp} (${row.timeFromNow})`}>
{row.timeLocal}
</div>
)}
{showLabels && (
<div className="logs-row__labels">
<LogLabels getRows={getRows} labels={row.uniqueLabels} onClickLabel={onClickLabel} />
</div>
)}
<div
className="logs-row__message"
onMouseEnter={this.onMouseOverMessage}
onMouseLeave={this.onMouseOutMessage}
>
<div
className={css`
position: relative;
`}
>
{showContext && context && (
<LogRowContext
row={row}
context={context}
errors={errors}
hasMoreContextRows={hasMoreContextRows}
onOutsideClick={this.toggleContext}
onLoadMoreContext={() => {
if (updateLimit) {
updateLimit();
}
}}
/>
)}
<span className={styles}>
{parsed && (
<Highlighter
autoEscape
highlightTag={FieldHighlight(this.onClickHighlight)}
textToHighlight={entry}
searchWords={parsedFieldHighlights}
highlightClassName="logs-row__field-highlight"
/>
)}
{!parsed && needsHighlighter && (
<Highlighter
textToHighlight={entry}
searchWords={highlights}
findChunks={findHighlightChunksInText}
highlightClassName={highlightClassName}
/>
)}
{hasAnsi && !parsed && !needsHighlighter && <LogMessageAnsi value={raw} />}
{!hasAnsi && !parsed && !needsHighlighter && entry}
{showFieldStats && (
<div className="logs-row__stats">
<LogLabelStats
stats={fieldStats}
label={fieldLabel}
value={fieldValue}
onClickClose={this.onClickClose}
rowCount={fieldCount}
/>
</div>
)}
</span>
{row.searchWords && row.searchWords.length > 0 && (
<span
onClick={this.onContextToggle}
className={css`
visibility: hidden;
white-space: nowrap;
position: relative;
z-index: ${showContext ? 1 : 0};
cursor: pointer;
.logs-row:hover & {
visibility: visible;
margin-left: 10px;
text-decoration: underline;
}
`}
>
{showContext ? 'Hide' : 'Show'} context
</span>
)}
</div>
</div>
</div>
)}
</div>
</div>
);
}}
</ThemeContext.Consumer>
);
}
render() {
const { showContext } = this.state;
if (showContext) {
return (
<>
<LogRowContextProvider row={this.props.row} getRowContext={this.props.getRowContext}>
{({ result, errors, hasMoreContextRows, updateLimit }) => {
return <>{this.renderLogRow(result, errors, hasMoreContextRows, updateLimit)}</>;
}}
</LogRowContextProvider>
</>
);
}
return this.renderLogRow();
}
}

View File

@ -0,0 +1,239 @@
import React, { useContext, useRef, useState, useLayoutEffect } from 'react';
import {
ThemeContext,
List,
GrafanaTheme,
selectThemeVariant,
ClickOutsideWrapper,
CustomScrollbar,
DataQueryError,
} 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 {
row: LogRowModel;
context: LogRowContextRows;
errors?: LogRowContextQueryErrors;
hasMoreContextRows: HasMoreContextRows;
onOutsideClick: () => void;
onLoadMoreContext: () => void;
}
const getLogRowContextStyles = (theme: GrafanaTheme) => {
const gradientTop = selectThemeVariant(
{
light: theme.colors.white,
dark: theme.colors.dark1,
},
theme.type
);
const gradientBottom = selectThemeVariant(
{
light: theme.colors.gray7,
dark: theme.colors.dark2,
},
theme.type
);
const boxShadowColor = selectThemeVariant(
{
light: theme.colors.gray5,
dark: theme.colors.black,
},
theme.type
);
const borderColor = selectThemeVariant(
{
light: theme.colors.gray5,
dark: theme.colors.dark9,
},
theme.type
);
return {
commonStyles: css`
position: absolute;
width: calc(100% + 20px);
left: -10px;
height: 250px;
z-index: 2;
overflow: hidden;
background: ${theme.colors.pageBg};
background: linear-gradient(180deg, ${gradientTop} 0%, ${gradientBottom} 104.25%);
box-shadow: 0px 2px 4px ${boxShadowColor}, 0px 0px 2px ${boxShadowColor};
border: 1px solid ${borderColor};
border-radius: ${theme.border.radius.md};
`,
header: css`
height: 30px;
padding: 0 10px;
display: flex;
align-items: center;
background: ${borderColor};
`,
logs: css`
height: 220px;
padding: 10px;
`,
};
};
interface LogRowContextGroupHeaderProps {
row: LogRowModel;
rows: Array<string | DataQueryError>;
onLoadMoreContext: () => void;
shouldScrollToBottom?: boolean;
canLoadMoreRows?: boolean;
}
interface LogRowContextGroupProps extends LogRowContextGroupHeaderProps {
rows: Array<string | DataQueryError>;
className: string;
error?: string;
}
const LogRowContextGroupHeader: React.FunctionComponent<LogRowContextGroupHeaderProps> = ({
row,
rows,
onLoadMoreContext,
canLoadMoreRows,
}) => {
const theme = useContext(ThemeContext);
const { header } = getLogRowContextStyles(theme);
// Filtering out the original row from the context.
// Loki requires a rowTimestamp+1ns for the following logs to be queried.
// We don't to ns-precision calculations in Loki log row context retrieval, hence the filtering here
// Also see: https://github.com/grafana/loki/issues/597
const logRowsToRender = rows.filter(contextRow => contextRow !== row.raw);
return (
<div className={header}>
<span
className={css`
opacity: 0.6;
`}
>
Found {logRowsToRender.length} rows.
</span>
{(rows.length >= 10 || (rows.length > 10 && rows.length % 10 !== 0)) && canLoadMoreRows && (
<span
className={css`
margin-left: 10px;
&:hover {
text-decoration: underline;
cursor: pointer;
}
`}
onClick={() => onLoadMoreContext()}
>
Load 10 more
</span>
)}
</div>
);
};
const LogRowContextGroup: React.FunctionComponent<LogRowContextGroupProps> = ({
row,
rows,
error,
className,
shouldScrollToBottom,
canLoadMoreRows,
onLoadMoreContext,
}) => {
const theme = useContext(ThemeContext);
const { commonStyles, logs } = getLogRowContextStyles(theme);
const [scrollTop, setScrollTop] = useState(0);
const listContainerRef = useRef<HTMLDivElement>();
useLayoutEffect(() => {
if (shouldScrollToBottom && listContainerRef.current) {
setScrollTop(listContainerRef.current.offsetHeight);
}
});
const headerProps = {
row,
rows,
onLoadMoreContext,
canLoadMoreRows,
};
return (
<div className={cx(className, commonStyles)}>
{/* When displaying "after" context */}
{shouldScrollToBottom && !error && <LogRowContextGroupHeader {...headerProps} />}
<div className={logs}>
<CustomScrollbar autoHide scrollTop={scrollTop}>
<div ref={listContainerRef}>
{!error && (
<List
items={rows}
renderItem={item => {
return (
<div
className={css`
padding: 5px 0;
`}
>
{item}
</div>
);
}}
/>
)}
{error && <Alert message={error} />}
</div>
</CustomScrollbar>
</div>
{/* When displaying "before" context */}
{!shouldScrollToBottom && !error && <LogRowContextGroupHeader {...headerProps} />}
</div>
);
};
export const LogRowContext: React.FunctionComponent<LogRowContextProps> = ({
row,
context,
errors,
onOutsideClick,
onLoadMoreContext,
hasMoreContextRows,
}) => {
return (
<ClickOutsideWrapper onClick={onOutsideClick}>
<div>
{context.after && (
<LogRowContextGroup
rows={context.after}
error={errors && errors.after}
row={row}
className={css`
top: -250px;
`}
shouldScrollToBottom
canLoadMoreRows={hasMoreContextRows.after}
onLoadMoreContext={onLoadMoreContext}
/>
)}
{context.before && (
<LogRowContextGroup
onLoadMoreContext={onLoadMoreContext}
canLoadMoreRows={hasMoreContextRows.before}
row={row}
rows={context.before}
error={errors && errors.before}
className={css`
top: 100%;
`}
/>
)}
</div>
</ClickOutsideWrapper>
);
};

View File

@ -0,0 +1,104 @@
import { LogRowModel } from 'app/core/logs_model';
import { LogRowContextQueryResponse, SeriesData, DataQueryResponse, DataQueryError } from '@grafana/ui';
import { useState, useEffect } from 'react';
import useAsync from 'react-use/lib/useAsync';
export interface LogRowContextRows {
before?: Array<string | DataQueryError>;
after?: Array<string | DataQueryError>;
}
export interface LogRowContextQueryErrors {
before?: string;
after?: string;
}
export interface HasMoreContextRows {
before: boolean;
after: boolean;
}
interface LogRowContextProviderProps {
row: LogRowModel;
getRowContext: (row: LogRowModel, limit: number) => Promise<DataQueryResponse>;
children: (props: {
result: LogRowContextRows;
errors: LogRowContextQueryErrors;
hasMoreContextRows: HasMoreContextRows;
updateLimit: () => void;
}) => JSX.Element;
}
export const LogRowContextProvider: React.FunctionComponent<LogRowContextProviderProps> = ({
getRowContext,
row,
children,
}) => {
const [limit, setLimit] = useState(10);
const [result, setResult] = useState<LogRowContextQueryResponse>(null);
const [errors, setErrors] = useState<LogRowContextQueryErrors>(null);
const [hasMoreContextRows, setHasMoreContextRows] = useState({
before: true,
after: true,
});
const { value } = useAsync(async () => {
const context = await getRowContext(row, limit);
return {
data: context.data.map(series => {
if ((series as SeriesData).rows) {
return (series as SeriesData).rows.map(row => row[1]);
} else {
return [series];
}
return [];
}),
};
}, [limit]);
useEffect(() => {
if (value) {
setResult(currentResult => {
let hasMoreLogsBefore = true,
hasMoreLogsAfter = true;
let beforeContextError, afterContextError;
if (currentResult && currentResult.data[0].length === value.data[0].length) {
hasMoreLogsBefore = false;
}
if (currentResult && currentResult.data[1].length === value.data[1].length) {
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;
});
}
}, [value]);
return children({
result: {
before: result ? result.data[0] : [],
after: result ? result.data[1] : [],
},
errors,
hasMoreContextRows,
updateLimit: () => setLimit(limit + 10),
});
};

View File

@ -5,7 +5,7 @@ import * as rangeUtil from '@grafana/ui/src/utils/rangeutil';
import { RawTimeRange, Switch, LogLevel, TimeZone, TimeRange, AbsoluteTimeRange } from '@grafana/ui';
import TimeSeries from 'app/core/time_series2';
import { LogsDedupDescription, LogsDedupStrategy, LogsModel, LogsMetaKind } from 'app/core/logs_model';
import { LogsDedupDescription, LogsDedupStrategy, LogsModel, LogsMetaKind, LogRowModel } from 'app/core/logs_model';
import ToggleButtonGroup, { ToggleButton } from 'app/core/components/ToggleButtonGroup/ToggleButtonGroup';
@ -60,6 +60,7 @@ interface Props {
onStopScanning?: () => void;
onDedupStrategyChange: (dedupStrategy: LogsDedupStrategy) => void;
onToggleLogLevel: (hiddenLogLevels: Set<LogLevel>) => void;
getRowContext?: (row: LogRowModel, limit: number) => Promise<any>;
}
interface State {
@ -252,6 +253,7 @@ export default class Logs extends PureComponent<Props, State> {
<LogRow
key={index}
getRows={getRows}
getRowContext={this.props.getRowContext}
highlighterExpressions={highlighterExpressions}
row={row}
showDuplicates={showDuplicates}
@ -268,6 +270,7 @@ export default class Logs extends PureComponent<Props, State> {
<LogRow
key={PREVIEW_LIMIT + index}
getRows={getRows}
getRowContext={this.props.getRowContext}
row={row}
showDuplicates={showDuplicates}
showLabels={showLabels && hasLabel}

View File

@ -1,10 +1,19 @@
import React, { PureComponent } from 'react';
import { hot } from 'react-hot-loader';
import { connect } from 'react-redux';
import { RawTimeRange, TimeRange, LogLevel, TimeZone, AbsoluteTimeRange, toUtc, dateTime } from '@grafana/ui';
import {
RawTimeRange,
TimeRange,
LogLevel,
TimeZone,
AbsoluteTimeRange,
toUtc,
dateTime,
DataSourceApi,
} from '@grafana/ui';
import { ExploreId, ExploreItemState } from 'app/types/explore';
import { LogsModel, LogsDedupStrategy } from 'app/core/logs_model';
import { LogsModel, LogsDedupStrategy, LogRowModel } from 'app/core/logs_model';
import { StoreState } from 'app/types';
import { changeDedupStrategy, changeTime } from './state/actions';
@ -15,6 +24,7 @@ import { deduplicatedLogsSelector, exploreItemUIStateSelector } from 'app/featur
import { getTimeZone } from '../profile/state/selectors';
interface LogsContainerProps {
datasourceInstance: DataSourceApi | null;
exploreId: ExploreId;
loading: boolean;
logsHighlighterExpressions?: string[];
@ -58,9 +68,20 @@ export class LogsContainer extends PureComponent<LogsContainerProps> {
});
};
getLogRowContext = async (row: LogRowModel, limit: number) => {
const { datasourceInstance } = this.props;
if (datasourceInstance) {
return datasourceInstance.getLogRowContext(row, limit);
}
return [];
};
render() {
const {
exploreId,
loading,
logsHighlighterExpressions,
logsResult,
@ -97,6 +118,7 @@ export class LogsContainer extends PureComponent<LogsContainerProps> {
scanRange={scanRange}
width={width}
hiddenLogLevels={hiddenLogLevels}
getRowContext={this.getLogRowContext}
/>
</Panel>
);
@ -106,7 +128,7 @@ export class LogsContainer extends PureComponent<LogsContainerProps> {
function mapStateToProps(state: StoreState, { exploreId }) {
const explore = state.explore;
const item: ExploreItemState = explore[exploreId];
const { logsHighlighterExpressions, logsResult, logIsLoading, scanning, scanRange, range } = item;
const { logsHighlighterExpressions, logsResult, logIsLoading, scanning, scanRange, range, datasourceInstance } = item;
const loading = logIsLoading;
const { dedupStrategy } = exploreItemUIStateSelector(item);
const hiddenLogLevels = new Set(item.hiddenLogLevels);
@ -124,6 +146,7 @@ function mapStateToProps(state: StoreState, { exploreId }) {
dedupStrategy,
hiddenLogLevels,
dedupedResult,
datasourceInstance,
};
}

View File

@ -379,7 +379,6 @@ export const itemReducer = reducerFactory<ExploreItemState>({} as ExploreItemSta
const { queryIntervals } = state;
const { result, resultType, latency } = action.payload;
const results = calculateResultsFromQueryTransactions(result, resultType, queryIntervals.intervalMs);
return {
...state,
graphResult: resultType === 'Graph' ? results.graphResult : state.graphResult,

View File

@ -21,6 +21,7 @@ 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;
@ -187,6 +188,83 @@ export class LokiDatasource extends DataSourceApi<LokiQuery, LokiOptions> {
return Math.ceil(date.valueOf() * 1e6);
}
prepareLogRowContextQueryTargets = (row: LogRowModel, limit: number) => {
const query = Object.keys(row.labels)
.map(label => {
return `${label}="${row.labels[label]}"`;
})
.join(',');
const contextTimeBuffer = 2 * 60 * 60 * 1000 * 1e6; // 2h buffer
const timeEpochNs = row.timeEpochMs * 1e6;
const commontTargetOptons = {
limit,
query: `{${query}}`,
};
return [
// Target for "before" context
{
...commontTargetOptons,
start: timeEpochNs - contextTimeBuffer,
end: timeEpochNs,
direction: 'BACKWARD',
},
// Target for "after" context
{
...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);
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);
}
}
// 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] };
});
};
testDatasource() {
return this._request('/api/prom/label')
.then(res => {

View File

@ -179,6 +179,7 @@ export interface ExploreItemState {
* Log query result to be displayed in the logs result viewer.
*/
logsResult?: LogsModel;
/**
* Query intervals for graph queries to determine how many datapoints to return.
* Needs to be updated when `datasourceInstance` or `containerWidth` is changed.

View File

@ -73,6 +73,7 @@ $column-horizontal-spacing: 10px;
padding-right: $column-horizontal-spacing;
border-top: 1px solid transparent;
border-bottom: 1px solid transparent;
height: 100%;
}
&:hover {

131
yarn.lock
View File

@ -4054,6 +4054,11 @@ boolbase@^1.0.0, boolbase@~1.0.0:
resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e"
integrity sha1-aN/1++YMUes3cl6p4+0xDcwed24=
bowser@^1.7.3:
version "1.9.4"
resolved "https://registry.yarnpkg.com/bowser/-/bowser-1.9.4.tgz#890c58a2813a9d3243704334fa81b96a5c150c9a"
integrity sha512-9IdMmj2KjigRq6oWhmwv1W36pDuA4STQZ8q6YO9um+x07xgYNCD3Oou+WP/3L1HNz7iqythGet3/p4wvc8AAwQ==
boxen@^1.2.1:
version "1.3.0"
resolved "https://registry.yarnpkg.com/boxen/-/boxen-1.3.0.tgz#55c6c39a8ba58d9c61ad22cd877532deb665a20b"
@ -5143,7 +5148,7 @@ copy-descriptor@^0.1.0:
resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d"
integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=
copy-to-clipboard@^3.0.8:
copy-to-clipboard@^3.0.8, copy-to-clipboard@^3.1.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/copy-to-clipboard/-/copy-to-clipboard-3.2.0.tgz#d2724a3ccbfed89706fac8a894872c979ac74467"
integrity sha512-eOZERzvCmxS8HWzugj4Uxl8OJxa7T2k1Gi0X5qavwydHIfuSHq2dTD09LOg/XyGq4Zpb5IsR/2OJ5lbOegz78w==
@ -5366,6 +5371,14 @@ css-declaration-sorter@^4.0.1:
postcss "^7.0.1"
timsort "^0.3.0"
css-in-js-utils@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/css-in-js-utils/-/css-in-js-utils-2.0.1.tgz#3b472b398787291b47cfe3e44fecfdd9e914ba99"
integrity sha512-PJF0SpJT+WdbVVt0AOYp9C8GnuruRlL/UFW7932nLWmFLQTaWEzTBQEx7/hn4BuV+WON75iAViSUJLiU3PKbpA==
dependencies:
hyphenate-style-name "^1.0.2"
isobject "^3.0.1"
css-loader@2.1.1, css-loader@^2.1.0:
version "2.1.1"
resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-2.1.1.tgz#d8254f72e412bb2238bb44dd674ffbef497333ea"
@ -5416,7 +5429,7 @@ css-tree@1.0.0-alpha.28:
mdn-data "~1.1.0"
source-map "^0.5.3"
css-tree@1.0.0-alpha.29:
css-tree@1.0.0-alpha.29, css-tree@^1.0.0-alpha.28:
version "1.0.0-alpha.29"
resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.0.0-alpha.29.tgz#3fa9d4ef3142cbd1c301e7664c1f352bd82f5a39"
integrity sha512-sRNb1XydwkW9IOci6iB2xmy8IGCj6r/fr+JWitvJ2JxQRPzN3T4AGGVWCMlVmVwM1gtgALJRmGIlWv5ppnGGkg==
@ -5541,7 +5554,7 @@ cssstyle@^1.0.0:
dependencies:
cssom "0.3.x"
csstype@^2.2.0, csstype@^2.5.2, csstype@^2.5.7:
csstype@^2.2.0, csstype@^2.5.2, csstype@^2.5.5, csstype@^2.5.7:
version "2.6.4"
resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.4.tgz#d585a6062096e324e7187f80e04f92bd0f00e37f"
integrity sha512-lAJUJP3M6HxFXbqtGRc0iZrdyeN+WzOWeY0q/VnFzI+kqVrYIzC7bWlKqCW7oCIdzoPkvfp82EVvrTlQ8zsWQg==
@ -6801,6 +6814,13 @@ error-ex@^1.2.0, error-ex@^1.3.1:
dependencies:
is-arrayish "^0.2.1"
error-stack-parser@^2.0.1:
version "2.0.2"
resolved "https://registry.yarnpkg.com/error-stack-parser/-/error-stack-parser-2.0.2.tgz#4ae8dbaa2bf90a8b450707b9149dcabca135520d"
integrity sha512-E1fPutRDdIj/hohG0UpT5mayXNCxXP9d+snxFsPU9X0XgccOumKraa3juDMwTUyi7+Bu5+mCGagjg4IYeNbOdw==
dependencies:
stackframe "^1.0.4"
es-abstract@^1.10.0, es-abstract@^1.11.0, es-abstract@^1.12.0, es-abstract@^1.13.0, es-abstract@^1.4.3, es-abstract@^1.5.0, es-abstract@^1.5.1, es-abstract@^1.7.0, es-abstract@^1.9.0:
version "1.13.0"
resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.13.0.tgz#ac86145fdd5099d8dd49558ccba2eaf9b88e24e9"
@ -7355,6 +7375,11 @@ fast-text-encoding@^1.0.0:
resolved "https://registry.yarnpkg.com/fast-text-encoding/-/fast-text-encoding-1.0.0.tgz#3e5ce8293409cfaa7177a71b9ca84e1b1e6f25ef"
integrity sha512-R9bHCvweUxxwkDwhjav5vxpFvdPGlVngtqmx4pIZfSUhM/Q4NiIUHB456BAf+Q1Nwu3HEZYONtu+Rya+af4jiQ==
fastest-stable-stringify@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/fastest-stable-stringify/-/fastest-stable-stringify-1.0.1.tgz#9122d406d4c9d98bea644a6b6853d5874b87b028"
integrity sha1-kSLUBtTJ2YvqZEpraFPVh0uHsCg=
fastparse@^1.1.1:
version "1.1.2"
resolved "https://registry.yarnpkg.com/fastparse/-/fastparse-1.1.2.tgz#91728c5a5942eced8531283c79441ee4122c35a9"
@ -8837,6 +8862,11 @@ husky@1.3.1:
run-node "^1.0.0"
slash "^2.0.0"
hyphenate-style-name@^1.0.2:
version "1.0.3"
resolved "https://registry.yarnpkg.com/hyphenate-style-name/-/hyphenate-style-name-1.0.3.tgz#097bb7fa0b8f1a9cf0bd5c734cf95899981a9b48"
integrity sha512-EcuixamT82oplpoJ2XU4pDtKGWQ7b00CD9f1ug9IaQ3p1bkHMiKCZ9ut9QDI6qsa6cpUuB+A/I+zLtdNK4n2DQ==
iconv-lite@0.4, iconv-lite@0.4.24, iconv-lite@^0.4.24, iconv-lite@^0.4.4, iconv-lite@~0.4.13:
version "0.4.24"
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
@ -9024,6 +9054,14 @@ init-package-json@^1.10.3:
validate-npm-package-license "^3.0.1"
validate-npm-package-name "^3.0.0"
inline-style-prefixer@^4.0.0:
version "4.0.2"
resolved "https://registry.yarnpkg.com/inline-style-prefixer/-/inline-style-prefixer-4.0.2.tgz#d390957d26f281255fe101da863158ac6eb60911"
integrity sha512-N8nVhwfYga9MiV9jWlwfdj1UDIaZlBFu4cJSJkIr7tZX7sHpHhGR5su1qdpW+7KPL8ISTvCIkcaFi/JdBknvPg==
dependencies:
bowser "^1.7.3"
css-in-js-utils "^2.0.0"
inquirer@6.2.1:
version "6.2.1"
resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-6.2.1.tgz#9943fc4882161bdb0b0c9276769c75b32dbfcd52"
@ -11565,6 +11603,20 @@ nan@^2.10.0, nan@^2.12.1, nan@^2.6.2:
resolved "https://registry.yarnpkg.com/nan/-/nan-2.13.2.tgz#f51dc7ae66ba7d5d55e1e6d4d8092e802c9aefe7"
integrity sha512-TghvYc72wlMGMVMluVo9WRJc0mB8KxxF/gZ4YYFy7V2ZQX9l7rgbPg7vjS9mt6U5HXODVFVI2bOduCzwOMv/lw==
nano-css@^5.1.0:
version "5.1.0"
resolved "https://registry.yarnpkg.com/nano-css/-/nano-css-5.1.0.tgz#03c2b4ea2edefd445ac0c0e0f2565ea62e2aa81a"
integrity sha512-08F1rBmp0JuAteOR/uk/c40q/+UxWr224m/ZCHjjgy8dhkFQptvNwj/408KYQc13PIV9aGvqmtUD49PqBB5Ppg==
dependencies:
css-tree "^1.0.0-alpha.28"
csstype "^2.5.5"
fastest-stable-stringify "^1.0.1"
inline-style-prefixer "^4.0.0"
rtl-css-js "^1.9.0"
sourcemap-codec "^1.4.1"
stacktrace-js "^2.0.0"
stylis "3.5.0"
nanomatch@^1.2.9:
version "1.2.13"
resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119"
@ -14100,7 +14152,7 @@ react-error-overlay@^5.1.4:
resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-5.1.5.tgz#884530fd055476c764eaa8ab13b8ecf1f57bbf2c"
integrity sha512-O9JRum1Zq/qCPFH5qVEvDDrVun8Jv9vbHtZXCR1EuRj9sKg1xJTlHxBzU6AkCzpvxRLuiY4OKImy3cDLQ+UTdg==
react-fast-compare@^2.0.2:
react-fast-compare@^2.0.2, react-fast-compare@^2.0.4:
version "2.0.4"
resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-2.0.4.tgz#e84b4d455b0fec113e0402c329352715196f81f9"
integrity sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw==
@ -14379,6 +14431,19 @@ react-transition-group@^2.2.1:
prop-types "^15.6.2"
react-lifecycles-compat "^3.0.4"
react-use@9.0.0:
version "9.0.0"
resolved "https://registry.yarnpkg.com/react-use/-/react-use-9.0.0.tgz#142bec53fa465db2a6e43c68a8c9ef2acc000592"
integrity sha512-jlXJneB96yl4VvAXDKyE6cmdIeWk0cO7Gomh870Qu0vXZ9YM2JjjR09E9vIPPPI2M27RWo2dZKXspv44Wxtoog==
dependencies:
copy-to-clipboard "^3.1.0"
nano-css "^5.1.0"
react-fast-compare "^2.0.4"
react-wait "^0.3.0"
screenfull "^4.1.0"
throttle-debounce "^2.0.1"
ts-easing "^0.2.0"
react-virtualized@9.21.0:
version "9.21.0"
resolved "https://registry.yarnpkg.com/react-virtualized/-/react-virtualized-9.21.0.tgz#8267c40ffb48db35b242a36dea85edcf280a6506"
@ -14391,6 +14456,11 @@ react-virtualized@9.21.0:
prop-types "^15.6.0"
react-lifecycles-compat "^3.0.4"
react-wait@^0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/react-wait/-/react-wait-0.3.0.tgz#0cdd4d919012451a5bc3ab0a16d00c6fd9a8c10b"
integrity sha512-kB5x/kMKWcn0uVr9gBdNz21/oGbQwEQnF3P9p6E9yLfJ9DRcKS0fagbgYMFI0YFOoyKDj+2q6Rwax0kTYJF37g==
react-window@1.7.1:
version "1.7.1"
resolved "https://registry.yarnpkg.com/react-window/-/react-window-1.7.1.tgz#c1db640415b97b85bc0a1c66eb82dadabca39b86"
@ -15206,6 +15276,13 @@ rsvp@^4.8.4:
resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-4.8.4.tgz#b50e6b34583f3dd89329a2f23a8a2be072845911"
integrity sha512-6FomvYPfs+Jy9TfXmBpBuMWNH94SgCsZmJKcanySzgNNP6LjWxBvyLTa9KaMfDDM5oxRfrKDB0r/qeRsLwnBfA==
rtl-css-js@^1.9.0:
version "1.11.0"
resolved "https://registry.yarnpkg.com/rtl-css-js/-/rtl-css-js-1.11.0.tgz#a7151930ef9d54656607d754ebb172ddfc9ef836"
integrity sha512-YnZ6jWxZxlWlcQAGF9vOmiF9bEmoQmSHE+wsrsiILkdK9HqiRPAIll4SY/QDzbvEu2lB2h62+hfg3TYzjnldbA==
dependencies:
"@babel/runtime" "^7.1.2"
run-async@^0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/run-async/-/run-async-0.1.0.tgz#c8ad4a5e110661e402a7d21b530e009f25f8e389"
@ -15400,6 +15477,11 @@ schema-utils@^1.0.0:
ajv-errors "^1.0.0"
ajv-keywords "^3.1.0"
screenfull@^4.1.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/screenfull/-/screenfull-4.2.0.tgz#d5252a5a0f56504719abbed9ebbcd9208115da03"
integrity sha512-qpyI9XbwuMJElWRP5vTgxkFAl4k7HpyhIqBFOZEwX9QBXn0MAuRSpn7LOc6/4CeSwoz61oBu1VPV+2fbIWC+5Q==
scss-tokenizer@^0.2.3:
version "0.2.3"
resolved "https://registry.yarnpkg.com/scss-tokenizer/-/scss-tokenizer-0.2.3.tgz#8eb06db9a9723333824d3f5530641149847ce5d1"
@ -15978,7 +16060,7 @@ source-map@^0.7.2, source-map@^0.7.3:
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383"
integrity sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==
sourcemap-codec@^1.4.4:
sourcemap-codec@^1.4.1, sourcemap-codec@^1.4.4:
version "1.4.4"
resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.4.tgz#c63ea927c029dd6bd9a2b7fa03b3fec02ad56e9f"
integrity sha512-CYAPYdBu34781kLHkaW3m6b/uUSyMOC2R61gcYMWooeuaGtjof86ZA/8T+qVPPt7np1085CR9hmMGrySwEc8Xg==
@ -16093,6 +16175,13 @@ stable@^0.1.8, stable@~0.1.3, stable@~0.1.5:
resolved "https://registry.yarnpkg.com/stable/-/stable-0.1.8.tgz#836eb3c8382fe2936feaf544631017ce7d47a3cf"
integrity sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==
stack-generator@^2.0.1:
version "2.0.3"
resolved "https://registry.yarnpkg.com/stack-generator/-/stack-generator-2.0.3.tgz#bb74385c67ffc4ccf3c4dee5831832d4e509c8a0"
integrity sha512-kdzGoqrnqsMxOEuXsXyQTmvWXZmG0f3Ql2GDx5NtmZs59sT2Bt9Vdyq0XdtxUi58q/+nxtbF9KOQ9HkV1QznGg==
dependencies:
stackframe "^1.0.4"
stack-parser@^0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/stack-parser/-/stack-parser-0.0.1.tgz#7d3b63a17887e9e2c2bf55dbd3318fe34a39d1e7"
@ -16103,6 +16192,28 @@ stack-utils@^1.0.1:
resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-1.0.2.tgz#33eba3897788558bebfc2db059dc158ec36cebb8"
integrity sha512-MTX+MeG5U994cazkjd/9KNAapsHnibjMLnfXodlkXw76JEea0UiNzrqidzo1emMwk7w5Qhc9jd4Bn9TBb1MFwA==
stackframe@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/stackframe/-/stackframe-1.0.4.tgz#357b24a992f9427cba6b545d96a14ed2cbca187b"
integrity sha512-to7oADIniaYwS3MhtCa/sQhrxidCCQiF/qp4/m5iN3ipf0Y7Xlri0f6eG29r08aL7JYl8n32AF3Q5GYBZ7K8vw==
stacktrace-gps@^3.0.1:
version "3.0.2"
resolved "https://registry.yarnpkg.com/stacktrace-gps/-/stacktrace-gps-3.0.2.tgz#33f8baa4467323ab2bd1816efa279942ba431ccc"
integrity sha512-9o+nWhiz5wFnrB3hBHs2PTyYrS60M1vvpSzHxwxnIbtY2q9Nt51hZvhrG1+2AxD374ecwyS+IUwfkHRE/2zuGg==
dependencies:
source-map "0.5.6"
stackframe "^1.0.4"
stacktrace-js@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/stacktrace-js/-/stacktrace-js-2.0.0.tgz#776ca646a95bc6c6b2b90776536a7fc72c6ddb58"
integrity sha1-d2ymRqlbxsayuQd2U2p/xyxt21g=
dependencies:
error-stack-parser "^2.0.1"
stack-generator "^2.0.1"
stacktrace-gps "^3.0.1"
staged-git-files@1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/staged-git-files/-/staged-git-files-1.1.2.tgz#4326d33886dc9ecfa29a6193bf511ba90a46454b"
@ -16410,6 +16521,11 @@ stylis-rule-sheet@^0.0.10:
resolved "https://registry.yarnpkg.com/stylis-rule-sheet/-/stylis-rule-sheet-0.0.10.tgz#44e64a2b076643f4b52e5ff71efc04d8c3c4a430"
integrity sha512-nTbZoaqoBnmK+ptANthb10ZRZOGC+EmTLLUxeYIuHNkEKcmKgXX1XWKkUBT2Ac4es3NybooPe0SmvKdhKJZAuw==
stylis@3.5.0:
version "3.5.0"
resolved "https://registry.yarnpkg.com/stylis/-/stylis-3.5.0.tgz#016fa239663d77f868fef5b67cf201c4b7c701e1"
integrity sha512-pP7yXN6dwMzAR29Q0mBrabPCe0/mNO1MSr93bhay+hcZondvMMTpeGyd8nbhYJdyperNT2DRxONQuUGcJr5iPw==
stylis@^3.5.0:
version "3.5.4"
resolved "https://registry.yarnpkg.com/stylis/-/stylis-3.5.4.tgz#f665f25f5e299cf3d64654ab949a57c768b73fbe"
@ -16665,6 +16781,11 @@ throat@^4.0.0:
resolved "https://registry.yarnpkg.com/throat/-/throat-4.1.0.tgz#89037cbc92c56ab18926e6ba4cbb200e15672a6a"
integrity sha1-iQN8vJLFarGJJua6TLsgDhVnKmo=
throttle-debounce@^2.0.1:
version "2.1.0"
resolved "https://registry.yarnpkg.com/throttle-debounce/-/throttle-debounce-2.1.0.tgz#257e648f0a56bd9e54fe0f132c4ab8611df4e1d5"
integrity sha512-AOvyNahXQuU7NN+VVvOOX+uW6FPaWdAOdRP5HfwYxAfCzXTFKRMoIMk+n+po318+ktcChx+F1Dd91G3YHeMKyg==
throttleit@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/throttleit/-/throttleit-1.0.0.tgz#9e785836daf46743145a5984b6268d828528ac6c"