[Logs UI] Don't break log stream on syntactically invalid KQL (#98191)

This commit is contained in:
Felix Stürmer 2021-04-27 20:06:16 +02:00 committed by GitHub
parent afc4aef9d5
commit 0b16688a24
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 383 additions and 210 deletions

View file

@ -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;

View file

@ -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>

View file

@ -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} />
);

View file

@ -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,
}));
};
}

View file

@ -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,
};

View file

@ -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,
});

View file

@ -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);

View file

@ -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 });

View file

@ -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)"`

View file

@ -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:'')"
/>
`);
});

View file

@ -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)
)('');

View file

@ -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)
)('');

View file

@ -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>;
};

View file

@ -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}>

View file

@ -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

View file

@ -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)`