mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[Logs UI] Don't break log stream on syntactically invalid KQL (#98191)
This commit is contained in:
parent
afc4aef9d5
commit
0b16688a24
16 changed files with 383 additions and 210 deletions
|
@ -7,7 +7,7 @@
|
|||
|
||||
import React, { useMemo, useCallback, useEffect } from 'react';
|
||||
import { noop } from 'lodash';
|
||||
import type { DataPublicPluginStart } from '../../../../../../src/plugins/data/public';
|
||||
import { DataPublicPluginStart, esQuery, Filter } from '../../../../../../src/plugins/data/public';
|
||||
import { euiStyled } from '../../../../../../src/plugins/kibana_react/common';
|
||||
import { LogEntryCursor } from '../../../common/log_entry';
|
||||
|
||||
|
@ -19,6 +19,7 @@ import { ScrollableLogTextStreamView } from '../logging/log_text_stream';
|
|||
import { LogColumnRenderConfiguration } from '../../utils/log_column_render_configuration';
|
||||
import { JsonValue } from '../../../../../../src/plugins/kibana_utils/common';
|
||||
import { Query } from '../../../../../../src/plugins/data/common';
|
||||
import { LogStreamErrorBoundary } from './log_stream_error_boundary';
|
||||
|
||||
interface LogStreamPluginDeps {
|
||||
data: DataPublicPluginStart;
|
||||
|
@ -57,25 +58,39 @@ type LogColumnDefinition =
|
|||
| MessageColumnDefinition
|
||||
| FieldColumnDefinition;
|
||||
|
||||
export interface LogStreamProps {
|
||||
export interface LogStreamProps extends LogStreamContentProps {
|
||||
height?: string | number;
|
||||
}
|
||||
|
||||
interface LogStreamContentProps {
|
||||
sourceId?: string;
|
||||
startTimestamp: number;
|
||||
endTimestamp: number;
|
||||
query?: string | Query | BuiltEsQuery;
|
||||
filters?: Filter[];
|
||||
center?: LogEntryCursor;
|
||||
highlight?: string;
|
||||
height?: string | number;
|
||||
columns?: LogColumnDefinition[];
|
||||
}
|
||||
|
||||
export const LogStream: React.FC<LogStreamProps> = ({
|
||||
export const LogStream: React.FC<LogStreamProps> = ({ height = 400, ...contentProps }) => {
|
||||
return (
|
||||
<LogStreamContainer style={{ height }}>
|
||||
<LogStreamErrorBoundary resetOnChange={[contentProps.query]}>
|
||||
<LogStreamContent {...contentProps} />
|
||||
</LogStreamErrorBoundary>
|
||||
</LogStreamContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export const LogStreamContent: React.FC<LogStreamContentProps> = ({
|
||||
sourceId = 'default',
|
||||
startTimestamp,
|
||||
endTimestamp,
|
||||
query,
|
||||
filters,
|
||||
center,
|
||||
highlight,
|
||||
height = '400px',
|
||||
columns,
|
||||
}) => {
|
||||
const customColumns = useMemo(
|
||||
|
@ -99,12 +114,21 @@ Read more at https://github.com/elastic/kibana/blob/master/src/plugins/kibana_re
|
|||
sourceConfiguration,
|
||||
loadSourceConfiguration,
|
||||
isLoadingSourceConfiguration,
|
||||
derivedIndexPattern,
|
||||
} = useLogSource({
|
||||
sourceId,
|
||||
fetch: services.http.fetch,
|
||||
indexPatternsService: services.data.indexPatterns,
|
||||
});
|
||||
|
||||
const parsedQuery = useMemo<BuiltEsQuery | undefined>(() => {
|
||||
if (typeof query === 'object' && 'bool' in query) {
|
||||
return mergeBoolQueries(query, esQuery.buildEsQuery(derivedIndexPattern, [], filters ?? []));
|
||||
} else {
|
||||
return esQuery.buildEsQuery(derivedIndexPattern, coerceToQueries(query), filters ?? []);
|
||||
}
|
||||
}, [derivedIndexPattern, filters, query]);
|
||||
|
||||
// Internal state
|
||||
const {
|
||||
entries,
|
||||
|
@ -119,7 +143,7 @@ Read more at https://github.com/elastic/kibana/blob/master/src/plugins/kibana_re
|
|||
sourceId,
|
||||
startTimestamp,
|
||||
endTimestamp,
|
||||
query,
|
||||
query: parsedQuery,
|
||||
center,
|
||||
columns: customColumns,
|
||||
});
|
||||
|
@ -138,8 +162,6 @@ Read more at https://github.com/elastic/kibana/blob/master/src/plugins/kibana_re
|
|||
[entries]
|
||||
);
|
||||
|
||||
const parsedHeight = typeof height === 'number' ? `${height}px` : height;
|
||||
|
||||
// Component lifetime
|
||||
useEffect(() => {
|
||||
loadSourceConfiguration();
|
||||
|
@ -170,37 +192,34 @@ Read more at https://github.com/elastic/kibana/blob/master/src/plugins/kibana_re
|
|||
);
|
||||
|
||||
return (
|
||||
<LogStreamContent height={parsedHeight}>
|
||||
<ScrollableLogTextStreamView
|
||||
target={center ? center : entries.length ? entries[entries.length - 1].cursor : null}
|
||||
columnConfigurations={columnConfigurations}
|
||||
items={streamItems}
|
||||
scale="medium"
|
||||
wrap={true}
|
||||
isReloading={isLoadingSourceConfiguration || isReloading}
|
||||
isLoadingMore={isLoadingMore}
|
||||
hasMoreBeforeStart={hasMoreBefore}
|
||||
hasMoreAfterEnd={hasMoreAfter}
|
||||
isStreaming={false}
|
||||
jumpToTarget={noop}
|
||||
reportVisibleInterval={handlePagination}
|
||||
reloadItems={fetchEntries}
|
||||
highlightedItem={highlight ?? null}
|
||||
currentHighlightKey={null}
|
||||
startDateExpression={''}
|
||||
endDateExpression={''}
|
||||
updateDateRange={noop}
|
||||
startLiveStreaming={noop}
|
||||
hideScrollbar={false}
|
||||
/>
|
||||
</LogStreamContent>
|
||||
<ScrollableLogTextStreamView
|
||||
target={center ? center : entries.length ? entries[entries.length - 1].cursor : null}
|
||||
columnConfigurations={columnConfigurations}
|
||||
items={streamItems}
|
||||
scale="medium"
|
||||
wrap={true}
|
||||
isReloading={isLoadingSourceConfiguration || isReloading}
|
||||
isLoadingMore={isLoadingMore}
|
||||
hasMoreBeforeStart={hasMoreBefore}
|
||||
hasMoreAfterEnd={hasMoreAfter}
|
||||
isStreaming={false}
|
||||
jumpToTarget={noop}
|
||||
reportVisibleInterval={handlePagination}
|
||||
reloadItems={fetchEntries}
|
||||
highlightedItem={highlight ?? null}
|
||||
currentHighlightKey={null}
|
||||
startDateExpression={''}
|
||||
endDateExpression={''}
|
||||
updateDateRange={noop}
|
||||
startLiveStreaming={noop}
|
||||
hideScrollbar={false}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const LogStreamContent = euiStyled.div<{ height: string }>`
|
||||
const LogStreamContainer = euiStyled.div`
|
||||
display: flex;
|
||||
background-color: ${(props) => props.theme.eui.euiColorEmptyShade};
|
||||
height: ${(props) => props.height};
|
||||
`;
|
||||
|
||||
function convertLogColumnDefinitionToLogSourceColumnDefinition(
|
||||
|
@ -227,6 +246,27 @@ function convertLogColumnDefinitionToLogSourceColumnDefinition(
|
|||
});
|
||||
}
|
||||
|
||||
const mergeBoolQueries = (firstQuery: BuiltEsQuery, secondQuery: BuiltEsQuery): BuiltEsQuery => ({
|
||||
bool: {
|
||||
must: [...firstQuery.bool.must, ...secondQuery.bool.must],
|
||||
filter: [...firstQuery.bool.filter, ...secondQuery.bool.filter],
|
||||
should: [...firstQuery.bool.should, ...secondQuery.bool.should],
|
||||
must_not: [...firstQuery.bool.must_not, ...secondQuery.bool.must_not],
|
||||
},
|
||||
});
|
||||
|
||||
const coerceToQueries = (value: undefined | string | Query): Query[] => {
|
||||
if (value == null) {
|
||||
return [];
|
||||
} else if (typeof value === 'string') {
|
||||
return [{ language: 'kuery', query: value }];
|
||||
} else if ('language' in value && 'query' in value) {
|
||||
return [value];
|
||||
}
|
||||
|
||||
return [];
|
||||
};
|
||||
|
||||
// Allow for lazy loading
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default LogStream;
|
||||
|
|
|
@ -9,7 +9,7 @@ import { CoreStart } from 'kibana/public';
|
|||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { Query, TimeRange, esQuery, Filter } from '../../../../../../src/plugins/data/public';
|
||||
import { Filter, Query, TimeRange } from '../../../../../../src/plugins/data/public';
|
||||
import {
|
||||
Embeddable,
|
||||
EmbeddableInput,
|
||||
|
@ -69,8 +69,6 @@ export class LogStreamEmbeddable extends Embeddable<LogStreamEmbeddableInput> {
|
|||
return;
|
||||
}
|
||||
|
||||
const parsedQuery = esQuery.buildEsQuery(undefined, this.input.query, this.input.filters);
|
||||
|
||||
const startTimestamp = datemathToEpochMillis(this.input.timeRange.from);
|
||||
const endTimestamp = datemathToEpochMillis(this.input.timeRange.to, 'up');
|
||||
|
||||
|
@ -86,7 +84,8 @@ export class LogStreamEmbeddable extends Embeddable<LogStreamEmbeddableInput> {
|
|||
startTimestamp={startTimestamp}
|
||||
endTimestamp={endTimestamp}
|
||||
height="100%"
|
||||
query={parsedQuery}
|
||||
query={this.input.query}
|
||||
filters={this.input.filters}
|
||||
/>
|
||||
</div>
|
||||
</EuiThemeProvider>
|
||||
|
|
|
@ -0,0 +1,62 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EuiCodeBlock, EuiEmptyPrompt } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import React from 'react';
|
||||
import { KQLSyntaxError } from '../../../../../../src/plugins/data/common';
|
||||
import { RenderErrorFunc, ResettableErrorBoundary } from '../resettable_error_boundary';
|
||||
|
||||
export const LogStreamErrorBoundary: React.FC<{ resetOnChange: any }> = ({
|
||||
children,
|
||||
resetOnChange = null,
|
||||
}) => {
|
||||
return (
|
||||
<ResettableErrorBoundary
|
||||
renderError={renderLogStreamErrorContent}
|
||||
resetOnChange={resetOnChange}
|
||||
>
|
||||
{children}
|
||||
</ResettableErrorBoundary>
|
||||
);
|
||||
};
|
||||
|
||||
const LogStreamErrorContent: React.FC<{
|
||||
error: any;
|
||||
}> = ({ error }) => {
|
||||
if (error instanceof KQLSyntaxError) {
|
||||
return (
|
||||
<EuiEmptyPrompt
|
||||
title={
|
||||
<FormattedMessage
|
||||
id="xpack.infra.logStream.kqlErrorTitle"
|
||||
defaultMessage="Invalid KQL expression"
|
||||
tagName="h2"
|
||||
/>
|
||||
}
|
||||
body={<EuiCodeBlock className="eui-textLeft">{error.message}</EuiCodeBlock>}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<EuiEmptyPrompt
|
||||
title={
|
||||
<FormattedMessage
|
||||
id="xpack.infra.logStream.unknownErrorTitle"
|
||||
defaultMessage="An error occurred"
|
||||
tagName="h2"
|
||||
/>
|
||||
}
|
||||
body={<EuiCodeBlock className="eui-textLeft">{error.message}</EuiCodeBlock>}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const renderLogStreamErrorContent: RenderErrorFunc = ({ latestError }) => (
|
||||
<LogStreamErrorContent error={latestError} />
|
||||
);
|
|
@ -0,0 +1,72 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import equal from 'fast-deep-equal';
|
||||
import React from 'react';
|
||||
|
||||
export interface RenderErrorFuncArgs {
|
||||
latestError: any;
|
||||
resetError: () => void;
|
||||
}
|
||||
|
||||
export type RenderErrorFunc = (renderErrorArgs: RenderErrorFuncArgs) => React.ReactNode;
|
||||
|
||||
interface ResettableErrorBoundaryProps<ResetOnChange> {
|
||||
renderError: RenderErrorFunc;
|
||||
resetOnChange: ResetOnChange;
|
||||
}
|
||||
|
||||
interface ResettableErrorBoundaryState {
|
||||
latestError: any;
|
||||
}
|
||||
|
||||
export class ResettableErrorBoundary<ResetOnChange> extends React.Component<
|
||||
ResettableErrorBoundaryProps<ResetOnChange>,
|
||||
ResettableErrorBoundaryState
|
||||
> {
|
||||
state: ResettableErrorBoundaryState = {
|
||||
latestError: undefined,
|
||||
};
|
||||
|
||||
componentDidUpdate({
|
||||
resetOnChange: prevResetOnChange,
|
||||
}: ResettableErrorBoundaryProps<ResetOnChange>) {
|
||||
const { resetOnChange } = this.props;
|
||||
const { latestError } = this.state;
|
||||
|
||||
if (latestError != null && !equal(resetOnChange, prevResetOnChange)) {
|
||||
this.resetError();
|
||||
}
|
||||
}
|
||||
|
||||
static getDerivedStateFromError(error: any) {
|
||||
return {
|
||||
latestError: error,
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
const { children, renderError } = this.props;
|
||||
const { latestError } = this.state;
|
||||
|
||||
if (latestError != null) {
|
||||
return renderError({
|
||||
latestError,
|
||||
resetError: this.resetError,
|
||||
});
|
||||
}
|
||||
|
||||
return children;
|
||||
}
|
||||
|
||||
resetError = () => {
|
||||
this.setState((previousState) => ({
|
||||
...previousState,
|
||||
latestError: undefined,
|
||||
}));
|
||||
};
|
||||
}
|
|
@ -5,95 +5,100 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { useState, useMemo } from 'react';
|
||||
import createContainer from 'constate';
|
||||
import { IIndexPattern } from 'src/plugins/data/public';
|
||||
import { esKuery } from '../../../../../../../src/plugins/data/public';
|
||||
import { convertKueryToElasticSearchQuery } from '../../../utils/kuery';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { useDebounce } from 'react-use';
|
||||
import { esQuery, IIndexPattern, Query } from '../../../../../../../src/plugins/data/public';
|
||||
|
||||
export interface KueryFilterQuery {
|
||||
kind: 'kuery';
|
||||
expression: string;
|
||||
type ParsedQuery = ReturnType<typeof esQuery.buildEsQuery>;
|
||||
|
||||
interface ILogFilterState {
|
||||
filterQuery: {
|
||||
parsedQuery: ParsedQuery;
|
||||
serializedQuery: string;
|
||||
originalQuery: Query;
|
||||
} | null;
|
||||
filterQueryDraft: Query;
|
||||
validationErrors: string[];
|
||||
}
|
||||
|
||||
export interface SerializedFilterQuery {
|
||||
query: KueryFilterQuery;
|
||||
serializedQuery: string;
|
||||
}
|
||||
|
||||
interface LogFilterInternalStateParams {
|
||||
filterQuery: SerializedFilterQuery | null;
|
||||
filterQueryDraft: KueryFilterQuery | null;
|
||||
}
|
||||
|
||||
export const logFilterInitialState: LogFilterInternalStateParams = {
|
||||
const initialLogFilterState: ILogFilterState = {
|
||||
filterQuery: null,
|
||||
filterQueryDraft: null,
|
||||
filterQueryDraft: {
|
||||
language: 'kuery',
|
||||
query: '',
|
||||
},
|
||||
validationErrors: [],
|
||||
};
|
||||
|
||||
export type LogFilterStateParams = Omit<LogFilterInternalStateParams, 'filterQuery'> & {
|
||||
filterQuery: SerializedFilterQuery['serializedQuery'] | null;
|
||||
filterQueryAsKuery: SerializedFilterQuery['query'] | null;
|
||||
isFilterQueryDraftValid: boolean;
|
||||
};
|
||||
export interface LogFilterCallbacks {
|
||||
setLogFilterQueryDraft: (expression: string) => void;
|
||||
applyLogFilterQuery: (expression: string) => void;
|
||||
}
|
||||
const validationDebounceTimeout = 1000; // milliseconds
|
||||
|
||||
export const useLogFilterState: (props: {
|
||||
indexPattern: IIndexPattern;
|
||||
}) => LogFilterStateParams & LogFilterCallbacks = ({ indexPattern }) => {
|
||||
const [state, setState] = useState(logFilterInitialState);
|
||||
const { filterQuery, filterQueryDraft } = state;
|
||||
export const useLogFilterState = ({ indexPattern }: { indexPattern: IIndexPattern }) => {
|
||||
const [logFilterState, setLogFilterState] = useState<ILogFilterState>(initialLogFilterState);
|
||||
|
||||
const setLogFilterQueryDraft = useMemo(() => {
|
||||
const setDraft = (payload: KueryFilterQuery) =>
|
||||
setState((prevState) => ({ ...prevState, filterQueryDraft: payload }));
|
||||
return (expression: string) =>
|
||||
setDraft({
|
||||
kind: 'kuery',
|
||||
expression,
|
||||
});
|
||||
const parseQuery = useCallback(
|
||||
(filterQuery: Query) => esQuery.buildEsQuery(indexPattern, filterQuery, []),
|
||||
[indexPattern]
|
||||
);
|
||||
|
||||
const setLogFilterQueryDraft = useCallback((filterQueryDraft: Query) => {
|
||||
setLogFilterState((previousLogFilterState) => ({
|
||||
...previousLogFilterState,
|
||||
filterQueryDraft,
|
||||
validationErrors: [],
|
||||
}));
|
||||
}, []);
|
||||
const applyLogFilterQuery = useMemo(() => {
|
||||
const applyQuery = (payload: SerializedFilterQuery) =>
|
||||
setState((prevState) => ({
|
||||
...prevState,
|
||||
filterQueryDraft: payload.query,
|
||||
filterQuery: payload,
|
||||
}));
|
||||
return (expression: string) =>
|
||||
applyQuery({
|
||||
query: {
|
||||
kind: 'kuery',
|
||||
expression,
|
||||
},
|
||||
serializedQuery: convertKueryToElasticSearchQuery(expression, indexPattern),
|
||||
|
||||
const [, cancelPendingValidation] = useDebounce(
|
||||
() => {
|
||||
setLogFilterState((previousLogFilterState) => {
|
||||
try {
|
||||
parseQuery(logFilterState.filterQueryDraft);
|
||||
return {
|
||||
...previousLogFilterState,
|
||||
validationErrors: [],
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
...previousLogFilterState,
|
||||
validationErrors: [`${error}`],
|
||||
};
|
||||
}
|
||||
});
|
||||
}, [indexPattern]);
|
||||
},
|
||||
validationDebounceTimeout,
|
||||
[logFilterState.filterQueryDraft, parseQuery]
|
||||
);
|
||||
|
||||
const isFilterQueryDraftValid = useMemo(() => {
|
||||
if (filterQueryDraft?.kind === 'kuery') {
|
||||
const applyLogFilterQuery = useCallback(
|
||||
(filterQuery: Query) => {
|
||||
cancelPendingValidation();
|
||||
try {
|
||||
esKuery.fromKueryExpression(filterQueryDraft.expression);
|
||||
} catch (err) {
|
||||
return false;
|
||||
const parsedQuery = parseQuery(filterQuery);
|
||||
setLogFilterState((previousLogFilterState) => ({
|
||||
...previousLogFilterState,
|
||||
filterQuery: {
|
||||
parsedQuery,
|
||||
serializedQuery: JSON.stringify(parsedQuery),
|
||||
originalQuery: filterQuery,
|
||||
},
|
||||
filterQueryDraft: filterQuery,
|
||||
validationErrors: [],
|
||||
}));
|
||||
} catch (error) {
|
||||
setLogFilterState((previousLogFilterState) => ({
|
||||
...previousLogFilterState,
|
||||
validationErrors: [`${error}`],
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}, [filterQueryDraft]);
|
||||
|
||||
const serializedFilterQuery = useMemo(() => (filterQuery ? filterQuery.serializedQuery : null), [
|
||||
filterQuery,
|
||||
]);
|
||||
},
|
||||
[cancelPendingValidation, parseQuery]
|
||||
);
|
||||
|
||||
return {
|
||||
...state,
|
||||
filterQueryAsKuery: state.filterQuery ? state.filterQuery.query : null,
|
||||
filterQuery: serializedFilterQuery,
|
||||
isFilterQueryDraftValid,
|
||||
filterQuery: logFilterState.filterQuery,
|
||||
filterQueryDraft: logFilterState.filterQueryDraft,
|
||||
isFilterQueryDraftValid: logFilterState.validationErrors.length === 0,
|
||||
setLogFilterQueryDraft,
|
||||
applyLogFilterQuery,
|
||||
};
|
||||
|
|
|
@ -5,43 +5,57 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import * as rt from 'io-ts';
|
||||
import React, { useContext } from 'react';
|
||||
import { LogFilterState, LogFilterStateParams } from './log_filter_state';
|
||||
import { Query } from '../../../../../../../src/plugins/data/public';
|
||||
import { replaceStateKeyInQueryString, UrlStateContainer } from '../../../utils/url_state';
|
||||
|
||||
type LogFilterUrlState = LogFilterStateParams['filterQueryAsKuery'];
|
||||
import { LogFilterState } from './log_filter_state';
|
||||
|
||||
export const WithLogFilterUrlState: React.FC = () => {
|
||||
const { filterQueryAsKuery, applyLogFilterQuery } = useContext(LogFilterState.Context);
|
||||
const { filterQuery, applyLogFilterQuery } = useContext(LogFilterState.Context);
|
||||
|
||||
return (
|
||||
<UrlStateContainer
|
||||
urlState={filterQueryAsKuery}
|
||||
urlState={filterQuery?.originalQuery}
|
||||
urlStateKey="logFilter"
|
||||
mapToUrlState={mapToFilterQuery}
|
||||
onChange={(urlState) => {
|
||||
if (urlState) {
|
||||
applyLogFilterQuery(urlState.expression);
|
||||
applyLogFilterQuery(urlState);
|
||||
}
|
||||
}}
|
||||
onInitialize={(urlState) => {
|
||||
if (urlState) {
|
||||
applyLogFilterQuery(urlState.expression);
|
||||
applyLogFilterQuery(urlState);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const mapToFilterQuery = (value: any): LogFilterUrlState | undefined =>
|
||||
value?.kind === 'kuery' && typeof value.expression === 'string'
|
||||
? {
|
||||
kind: value.kind,
|
||||
expression: value.expression,
|
||||
}
|
||||
: undefined;
|
||||
const mapToFilterQuery = (value: any): Query | undefined => {
|
||||
if (legacyFilterQueryUrlStateRT.is(value)) {
|
||||
// migrate old url state
|
||||
return {
|
||||
language: value.kind,
|
||||
query: value.expression,
|
||||
};
|
||||
} else if (filterQueryUrlStateRT.is(value)) {
|
||||
return value;
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
export const replaceLogFilterInQueryString = (expression: string) =>
|
||||
replaceStateKeyInQueryString<LogFilterUrlState>('logFilter', {
|
||||
kind: 'kuery',
|
||||
expression,
|
||||
});
|
||||
export const replaceLogFilterInQueryString = (query: Query) =>
|
||||
replaceStateKeyInQueryString<Query>('logFilter', query);
|
||||
|
||||
const filterQueryUrlStateRT = rt.type({
|
||||
language: rt.string,
|
||||
query: rt.string,
|
||||
});
|
||||
|
||||
const legacyFilterQueryUrlStateRT = rt.type({
|
||||
kind: rt.literal('kuery'),
|
||||
expression: rt.string,
|
||||
});
|
||||
|
|
|
@ -5,11 +5,11 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { useState, useCallback, useEffect, useMemo } from 'react';
|
||||
import createContainer from 'constate';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import usePrevious from 'react-use/lib/usePrevious';
|
||||
import useSetState from 'react-use/lib/useSetState';
|
||||
import { esKuery, esQuery, Query } from '../../../../../../../src/plugins/data/public';
|
||||
import { esQuery } from '../../../../../../../src/plugins/data/public';
|
||||
import { LogEntry, LogEntryCursor } from '../../../../common/log_entry';
|
||||
import { useSubscription } from '../../../utils/use_observable';
|
||||
import { LogSourceConfigurationProperties } from '../log_source';
|
||||
|
@ -23,7 +23,7 @@ interface LogStreamProps {
|
|||
sourceId: string;
|
||||
startTimestamp: number;
|
||||
endTimestamp: number;
|
||||
query?: string | Query | BuiltEsQuery;
|
||||
query?: BuiltEsQuery;
|
||||
center?: LogEntryCursor;
|
||||
columns?: LogSourceConfigurationProperties['logColumns'];
|
||||
}
|
||||
|
@ -77,27 +77,15 @@ export function useLogStream({
|
|||
}
|
||||
}, [prevEndTimestamp, endTimestamp, setState]);
|
||||
|
||||
const parsedQuery = useMemo(() => {
|
||||
if (!query) {
|
||||
return undefined;
|
||||
} else if (typeof query === 'string') {
|
||||
return esKuery.toElasticsearchQuery(esKuery.fromKueryExpression(query));
|
||||
} else if ('language' in query) {
|
||||
return getEsQueryFromQueryObject(query);
|
||||
} else {
|
||||
return query;
|
||||
}
|
||||
}, [query]);
|
||||
|
||||
const commonFetchArguments = useMemo(
|
||||
() => ({
|
||||
sourceId,
|
||||
startTimestamp,
|
||||
endTimestamp,
|
||||
query: parsedQuery,
|
||||
query,
|
||||
columnOverrides: columns,
|
||||
}),
|
||||
[columns, endTimestamp, parsedQuery, sourceId, startTimestamp]
|
||||
[columns, endTimestamp, query, sourceId, startTimestamp]
|
||||
);
|
||||
|
||||
const {
|
||||
|
@ -268,13 +256,4 @@ export function useLogStream({
|
|||
};
|
||||
}
|
||||
|
||||
function getEsQueryFromQueryObject(query: Query) {
|
||||
switch (query.language) {
|
||||
case 'kuery':
|
||||
return esKuery.toElasticsearchQuery(esKuery.fromKueryExpression(query.query as string));
|
||||
case 'lucene':
|
||||
return esQuery.luceneStringToDsl(query.query as string);
|
||||
}
|
||||
}
|
||||
|
||||
export const [LogStreamProvider, useLogStreamContext] = createContainer(useLogStream);
|
||||
|
|
|
@ -7,12 +7,11 @@
|
|||
|
||||
import { useContext } from 'react';
|
||||
import useThrottle from 'react-use/lib/useThrottle';
|
||||
|
||||
import { RendererFunction } from '../../../utils/typed_react';
|
||||
import { LogSummaryBuckets, useLogSummary } from './log_summary';
|
||||
import { LogFilterState } from '../log_filter';
|
||||
import { LogPositionState } from '../log_position';
|
||||
import { useLogSourceContext } from '../log_source';
|
||||
import { LogSummaryBuckets, useLogSummary } from './log_summary';
|
||||
|
||||
const FETCH_THROTTLE_INTERVAL = 3000;
|
||||
|
||||
|
@ -37,7 +36,7 @@ export const WithSummary = ({
|
|||
sourceId,
|
||||
throttledStartTimestamp,
|
||||
throttledEndTimestamp,
|
||||
filterQuery
|
||||
filterQuery?.serializedQuery ?? null
|
||||
);
|
||||
|
||||
return children({ buckets, start, end });
|
||||
|
|
|
@ -66,7 +66,7 @@ describe('LinkToLogsPage component', () => {
|
|||
const searchParams = new URLSearchParams(history.location.search);
|
||||
expect(searchParams.get('sourceId')).toEqual('default');
|
||||
expect(searchParams.get('logFilter')).toMatchInlineSnapshot(
|
||||
`"(expression:'FILTER_FIELD:FILTER_VALUE',kind:kuery)"`
|
||||
`"(language:kuery,query:'FILTER_FIELD:FILTER_VALUE')"`
|
||||
);
|
||||
expect(searchParams.get('logPosition')).toMatchInlineSnapshot(
|
||||
`"(end:'2019-02-20T14:58:09.404Z',position:(tiebreaker:0,time:1550671089404),start:'2019-02-20T12:58:09.404Z',streamLive:!f)"`
|
||||
|
@ -86,7 +86,7 @@ describe('LinkToLogsPage component', () => {
|
|||
|
||||
const searchParams = new URLSearchParams(history.location.search);
|
||||
expect(searchParams.get('sourceId')).toEqual('OTHER_SOURCE');
|
||||
expect(searchParams.get('logFilter')).toMatchInlineSnapshot(`"(expression:'',kind:kuery)"`);
|
||||
expect(searchParams.get('logFilter')).toMatchInlineSnapshot(`"(language:kuery,query:'')"`);
|
||||
expect(searchParams.get('logPosition')).toEqual(null);
|
||||
});
|
||||
});
|
||||
|
@ -106,7 +106,7 @@ describe('LinkToLogsPage component', () => {
|
|||
const searchParams = new URLSearchParams(history.location.search);
|
||||
expect(searchParams.get('sourceId')).toEqual('default');
|
||||
expect(searchParams.get('logFilter')).toMatchInlineSnapshot(
|
||||
`"(expression:'FILTER_FIELD:FILTER_VALUE',kind:kuery)"`
|
||||
`"(language:kuery,query:'FILTER_FIELD:FILTER_VALUE')"`
|
||||
);
|
||||
expect(searchParams.get('logPosition')).toMatchInlineSnapshot(
|
||||
`"(end:'2019-02-20T14:58:09.404Z',position:(tiebreaker:0,time:1550671089404),start:'2019-02-20T12:58:09.404Z',streamLive:!f)"`
|
||||
|
@ -126,7 +126,7 @@ describe('LinkToLogsPage component', () => {
|
|||
|
||||
const searchParams = new URLSearchParams(history.location.search);
|
||||
expect(searchParams.get('sourceId')).toEqual('OTHER_SOURCE');
|
||||
expect(searchParams.get('logFilter')).toMatchInlineSnapshot(`"(expression:'',kind:kuery)"`);
|
||||
expect(searchParams.get('logFilter')).toMatchInlineSnapshot(`"(language:kuery,query:'')"`);
|
||||
expect(searchParams.get('logPosition')).toEqual(null);
|
||||
});
|
||||
});
|
||||
|
@ -146,7 +146,7 @@ describe('LinkToLogsPage component', () => {
|
|||
const searchParams = new URLSearchParams(history.location.search);
|
||||
expect(searchParams.get('sourceId')).toEqual('default');
|
||||
expect(searchParams.get('logFilter')).toMatchInlineSnapshot(
|
||||
`"(expression:'HOST_FIELD: HOST_NAME',kind:kuery)"`
|
||||
`"(language:kuery,query:'HOST_FIELD: HOST_NAME')"`
|
||||
);
|
||||
expect(searchParams.get('logPosition')).toEqual(null);
|
||||
});
|
||||
|
@ -167,7 +167,7 @@ describe('LinkToLogsPage component', () => {
|
|||
const searchParams = new URLSearchParams(history.location.search);
|
||||
expect(searchParams.get('sourceId')).toEqual('default');
|
||||
expect(searchParams.get('logFilter')).toMatchInlineSnapshot(
|
||||
`"(expression:'(HOST_FIELD: HOST_NAME) and (FILTER_FIELD:FILTER_VALUE)',kind:kuery)"`
|
||||
`"(language:kuery,query:'(HOST_FIELD: HOST_NAME) and (FILTER_FIELD:FILTER_VALUE)')"`
|
||||
);
|
||||
expect(searchParams.get('logPosition')).toMatchInlineSnapshot(
|
||||
`"(end:'2019-02-20T14:58:09.404Z',position:(tiebreaker:0,time:1550671089404),start:'2019-02-20T12:58:09.404Z',streamLive:!f)"`
|
||||
|
@ -188,7 +188,7 @@ describe('LinkToLogsPage component', () => {
|
|||
const searchParams = new URLSearchParams(history.location.search);
|
||||
expect(searchParams.get('sourceId')).toEqual('OTHER_SOURCE');
|
||||
expect(searchParams.get('logFilter')).toMatchInlineSnapshot(
|
||||
`"(expression:'HOST_FIELD: HOST_NAME',kind:kuery)"`
|
||||
`"(language:kuery,query:'HOST_FIELD: HOST_NAME')"`
|
||||
);
|
||||
expect(searchParams.get('logPosition')).toEqual(null);
|
||||
});
|
||||
|
@ -223,7 +223,7 @@ describe('LinkToLogsPage component', () => {
|
|||
const searchParams = new URLSearchParams(history.location.search);
|
||||
expect(searchParams.get('sourceId')).toEqual('default');
|
||||
expect(searchParams.get('logFilter')).toMatchInlineSnapshot(
|
||||
`"(expression:'CONTAINER_FIELD: CONTAINER_ID',kind:kuery)"`
|
||||
`"(language:kuery,query:'CONTAINER_FIELD: CONTAINER_ID')"`
|
||||
);
|
||||
expect(searchParams.get('logPosition')).toEqual(null);
|
||||
});
|
||||
|
@ -244,7 +244,7 @@ describe('LinkToLogsPage component', () => {
|
|||
const searchParams = new URLSearchParams(history.location.search);
|
||||
expect(searchParams.get('sourceId')).toEqual('default');
|
||||
expect(searchParams.get('logFilter')).toMatchInlineSnapshot(
|
||||
`"(expression:'(CONTAINER_FIELD: CONTAINER_ID) and (FILTER_FIELD:FILTER_VALUE)',kind:kuery)"`
|
||||
`"(language:kuery,query:'(CONTAINER_FIELD: CONTAINER_ID) and (FILTER_FIELD:FILTER_VALUE)')"`
|
||||
);
|
||||
expect(searchParams.get('logPosition')).toMatchInlineSnapshot(
|
||||
`"(end:'2019-02-20T14:58:09.404Z',position:(tiebreaker:0,time:1550671089404),start:'2019-02-20T12:58:09.404Z',streamLive:!f)"`
|
||||
|
@ -281,7 +281,7 @@ describe('LinkToLogsPage component', () => {
|
|||
const searchParams = new URLSearchParams(history.location.search);
|
||||
expect(searchParams.get('sourceId')).toEqual('default');
|
||||
expect(searchParams.get('logFilter')).toMatchInlineSnapshot(
|
||||
`"(expression:'POD_FIELD: POD_UID',kind:kuery)"`
|
||||
`"(language:kuery,query:'POD_FIELD: POD_UID')"`
|
||||
);
|
||||
expect(searchParams.get('logPosition')).toEqual(null);
|
||||
});
|
||||
|
@ -300,7 +300,7 @@ describe('LinkToLogsPage component', () => {
|
|||
const searchParams = new URLSearchParams(history.location.search);
|
||||
expect(searchParams.get('sourceId')).toEqual('default');
|
||||
expect(searchParams.get('logFilter')).toMatchInlineSnapshot(
|
||||
`"(expression:'(POD_FIELD: POD_UID) and (FILTER_FIELD:FILTER_VALUE)',kind:kuery)"`
|
||||
`"(language:kuery,query:'(POD_FIELD: POD_UID) and (FILTER_FIELD:FILTER_VALUE)')"`
|
||||
);
|
||||
expect(searchParams.get('logPosition')).toMatchInlineSnapshot(
|
||||
`"(end:'2019-02-20T14:58:09.404Z',position:(tiebreaker:0,time:1550671089404),start:'2019-02-20T12:58:09.404Z',streamLive:!f)"`
|
||||
|
|
|
@ -20,7 +20,7 @@ describe('RedirectToLogs component', () => {
|
|||
|
||||
expect(component).toMatchInlineSnapshot(`
|
||||
<Redirect
|
||||
to="/stream?sourceId=default&logPosition=(end:'2019-02-20T14:58:09.404Z',position:(tiebreaker:0,time:1550671089404),start:'2019-02-20T12:58:09.404Z',streamLive:!f)&logFilter=(expression:'',kind:kuery)"
|
||||
to="/stream?sourceId=default&logPosition=(end:'2019-02-20T14:58:09.404Z',position:(tiebreaker:0,time:1550671089404),start:'2019-02-20T12:58:09.404Z',streamLive:!f)&logFilter=(language:kuery,query:'')"
|
||||
/>
|
||||
`);
|
||||
});
|
||||
|
@ -34,7 +34,7 @@ describe('RedirectToLogs component', () => {
|
|||
|
||||
expect(component).toMatchInlineSnapshot(`
|
||||
<Redirect
|
||||
to="/stream?sourceId=default&logPosition=(end:'2019-02-20T14:58:09.404Z',position:(tiebreaker:0,time:1550671089404),start:'2019-02-20T12:58:09.404Z',streamLive:!f)&logFilter=(expression:'FILTER_FIELD:FILTER_VALUE',kind:kuery)"
|
||||
to="/stream?sourceId=default&logPosition=(end:'2019-02-20T14:58:09.404Z',position:(tiebreaker:0,time:1550671089404),start:'2019-02-20T12:58:09.404Z',streamLive:!f)&logFilter=(language:kuery,query:'FILTER_FIELD:FILTER_VALUE')"
|
||||
/>
|
||||
`);
|
||||
});
|
||||
|
@ -46,7 +46,7 @@ describe('RedirectToLogs component', () => {
|
|||
|
||||
expect(component).toMatchInlineSnapshot(`
|
||||
<Redirect
|
||||
to="/stream?sourceId=SOME-OTHER-SOURCE&logFilter=(expression:'',kind:kuery)"
|
||||
to="/stream?sourceId=SOME-OTHER-SOURCE&logFilter=(language:kuery,query:'')"
|
||||
/>
|
||||
`);
|
||||
});
|
||||
|
|
|
@ -26,7 +26,7 @@ export const RedirectToLogs = ({ location, match }: RedirectToLogsProps) => {
|
|||
const sourceId = match.params.sourceId || 'default';
|
||||
const filter = getFilterFromLocation(location);
|
||||
const searchString = flowRight(
|
||||
replaceLogFilterInQueryString(filter),
|
||||
replaceLogFilterInQueryString({ language: 'kuery', query: filter }),
|
||||
replaceLogPositionInQueryString(getTimeFromLocation(location)),
|
||||
replaceSourceIdInQueryString(sourceId)
|
||||
)('');
|
||||
|
|
|
@ -68,7 +68,7 @@ export const RedirectToNodeLogs = ({
|
|||
const filter = userFilter ? `(${nodeFilter}) and (${userFilter})` : nodeFilter;
|
||||
|
||||
const searchString = flowRight(
|
||||
replaceLogFilterInQueryString(filter),
|
||||
replaceLogFilterInQueryString({ language: 'kuery', query: filter }),
|
||||
replaceLogPositionInQueryString(getTimeFromLocation(location)),
|
||||
replaceSourceIdInQueryString(sourceId)
|
||||
)('');
|
||||
|
|
|
@ -50,28 +50,30 @@ const LogEntriesStateProvider: React.FC = ({ children }) => {
|
|||
const { startTimestamp, endTimestamp, targetPosition, isInitialized } = useContext(
|
||||
LogPositionState.Context
|
||||
);
|
||||
const { filterQueryAsKuery } = useContext(LogFilterState.Context);
|
||||
const { filterQuery } = useContext(LogFilterState.Context);
|
||||
|
||||
// Don't render anything if the date range is incorrect.
|
||||
if (!startTimestamp || !endTimestamp) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const logStreamProps = {
|
||||
sourceId,
|
||||
startTimestamp,
|
||||
endTimestamp,
|
||||
query: filterQueryAsKuery?.expression ?? undefined,
|
||||
center: targetPosition ?? undefined,
|
||||
};
|
||||
|
||||
// Don't initialize the entries until the position has been fully intialized.
|
||||
// See `<WithLogPositionUrlState />`
|
||||
if (!isInitialized) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <LogStreamProvider {...logStreamProps}>{children}</LogStreamProvider>;
|
||||
return (
|
||||
<LogStreamProvider
|
||||
sourceId={sourceId}
|
||||
startTimestamp={startTimestamp}
|
||||
endTimestamp={endTimestamp}
|
||||
query={filterQuery?.parsedQuery}
|
||||
center={targetPosition ?? undefined}
|
||||
>
|
||||
{children}
|
||||
</LogStreamProvider>
|
||||
);
|
||||
};
|
||||
|
||||
const LogHighlightsStateProvider: React.FC = ({ children }) => {
|
||||
|
@ -86,7 +88,7 @@ const LogHighlightsStateProvider: React.FC = ({ children }) => {
|
|||
entriesEnd: bottomCursor,
|
||||
centerCursor: entries.length > 0 ? entries[Math.floor(entries.length / 2)].cursor : null,
|
||||
size: entries.length,
|
||||
filterQuery,
|
||||
filterQuery: filterQuery?.serializedQuery ?? null,
|
||||
};
|
||||
return <LogHighlightsState.Provider {...highlightsProps}>{children}</LogHighlightsState.Provider>;
|
||||
};
|
||||
|
|
|
@ -62,25 +62,18 @@ export const LogsToolbar = () => {
|
|||
iconType="search"
|
||||
indexPatterns={[derivedIndexPattern]}
|
||||
isInvalid={!isFilterQueryDraftValid}
|
||||
onChange={(expression: Query) => {
|
||||
if (typeof expression.query === 'string') {
|
||||
setSurroundingLogsId(null);
|
||||
setLogFilterQueryDraft(expression.query);
|
||||
}
|
||||
onChange={(query: Query) => {
|
||||
setSurroundingLogsId(null);
|
||||
setLogFilterQueryDraft(query);
|
||||
}}
|
||||
onSubmit={(expression: Query) => {
|
||||
if (typeof expression.query === 'string') {
|
||||
setSurroundingLogsId(null);
|
||||
applyLogFilterQuery(expression.query);
|
||||
}
|
||||
onSubmit={(query: Query) => {
|
||||
setSurroundingLogsId(null);
|
||||
applyLogFilterQuery(query);
|
||||
}}
|
||||
placeholder={i18n.translate('xpack.infra.logsPage.toolbar.kqlSearchFieldPlaceholder', {
|
||||
defaultMessage: 'Search for log entries… (e.g. host.name:host-1)',
|
||||
})}
|
||||
query={{
|
||||
query: filterQueryDraft?.expression ?? '',
|
||||
language: filterQueryDraft?.kind ?? 'kuery',
|
||||
}}
|
||||
query={filterQueryDraft}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
*/
|
||||
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { useThrottle } from 'react-use';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiFieldSearch } from '@elastic/eui';
|
||||
|
@ -26,16 +27,21 @@ const TabComponent = (props: TabProps) => {
|
|||
const { nodeType } = useWaffleOptionsContext();
|
||||
const { options, node } = props;
|
||||
|
||||
const filter = useMemo(() => {
|
||||
let query = options.fields
|
||||
? `${findInventoryFields(nodeType, options.fields).id}: "${node.id}"`
|
||||
: ``;
|
||||
const throttledTextQuery = useThrottle(textQuery, textQueryThrottleInterval);
|
||||
|
||||
if (textQuery) {
|
||||
query += ` and message: ${textQuery}`;
|
||||
}
|
||||
return query;
|
||||
}, [options, nodeType, node.id, textQuery]);
|
||||
const filter = useMemo(() => {
|
||||
const query = [
|
||||
...(options.fields != null
|
||||
? [`${findInventoryFields(nodeType, options.fields).id}: "${node.id}"`]
|
||||
: []),
|
||||
...(throttledTextQuery !== '' ? [throttledTextQuery] : []),
|
||||
].join(' and ');
|
||||
|
||||
return {
|
||||
language: 'kuery',
|
||||
query,
|
||||
};
|
||||
}, [options.fields, nodeType, node.id, throttledTextQuery]);
|
||||
|
||||
const onQueryChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setTextQuery(e.target.value);
|
||||
|
@ -89,3 +95,5 @@ export const LogsTab = {
|
|||
}),
|
||||
content: TabComponent,
|
||||
};
|
||||
|
||||
const textQueryThrottleInterval = 1000; // milliseconds
|
||||
|
|
|
@ -45,7 +45,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
|
|||
|
||||
expect(parsedUrl.pathname).to.be('/app/logs/stream');
|
||||
expect(parsedUrl.searchParams.get('logFilter')).to.be(
|
||||
`(expression:'trace.id:${traceId}',kind:kuery)`
|
||||
`(language:kuery,query:'trace.id:${traceId}')`
|
||||
);
|
||||
expect(parsedUrl.searchParams.get('logPosition')).to.be(
|
||||
`(end:'${endDate}',position:(tiebreaker:0,time:${timestamp}),start:'${startDate}',streamLive:!f)`
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue