Merge pull request #4163 from influxdata/enhancement/log-search-operators
Enhance log search paramspull/4192/head
commit
98c28beab8
|
@ -66,7 +66,6 @@ export enum ActionTypes {
|
|||
SetHistogramData = 'LOGS_SET_HISTOGRAM_DATA',
|
||||
SetTableQueryConfig = 'LOGS_SET_TABLE_QUERY_CONFIG',
|
||||
SetTableData = 'LOGS_SET_TABLE_DATA',
|
||||
SetSearchTerm = 'LOGS_SET_SEARCH_TERM',
|
||||
AddFilter = 'LOGS_ADD_FILTER',
|
||||
RemoveFilter = 'LOGS_REMOVE_FILTER',
|
||||
ChangeFilter = 'LOGS_CHANGE_FILTER',
|
||||
|
@ -228,13 +227,6 @@ interface SetTableData {
|
|||
}
|
||||
}
|
||||
|
||||
interface SetSearchTerm {
|
||||
type: ActionTypes.SetSearchTerm
|
||||
payload: {
|
||||
searchTerm: string
|
||||
}
|
||||
}
|
||||
|
||||
export interface SetConfigsAction {
|
||||
type: ActionTypes.SetConfig
|
||||
payload: {
|
||||
|
@ -253,7 +245,6 @@ export type Action =
|
|||
| SetHistogramData
|
||||
| SetTableData
|
||||
| SetTableQueryConfig
|
||||
| SetSearchTerm
|
||||
| AddFilterAction
|
||||
| RemoveFilterAction
|
||||
| ChangeFilterAction
|
||||
|
@ -547,14 +538,6 @@ export const executeQueriesAsync = () => async dispatch => {
|
|||
}
|
||||
}
|
||||
|
||||
export const setSearchTermAsync = (searchTerm: string) => async dispatch => {
|
||||
dispatch({
|
||||
type: ActionTypes.SetSearchTerm,
|
||||
payload: {searchTerm},
|
||||
})
|
||||
dispatch(executeQueriesAsync())
|
||||
}
|
||||
|
||||
export const setHistogramQueryConfigAsync = () => async (
|
||||
dispatch,
|
||||
getState: GetState
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import React, {PureComponent, ChangeEvent, KeyboardEvent} from 'react'
|
||||
|
||||
interface Props {
|
||||
searchString: string
|
||||
onSearch: (value: string) => void
|
||||
}
|
||||
|
||||
|
@ -14,7 +13,7 @@ class LogsSearchBar extends PureComponent<Props, State> {
|
|||
super(props)
|
||||
|
||||
this.state = {
|
||||
searchTerm: props.searchString,
|
||||
searchTerm: '',
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -45,6 +44,7 @@ class LogsSearchBar extends PureComponent<Props, State> {
|
|||
|
||||
private handleSearch = () => {
|
||||
this.props.onSearch(this.state.searchTerm)
|
||||
this.setState({searchTerm: ''})
|
||||
}
|
||||
|
||||
private handleInputKeyDown = (e: KeyboardEvent<HTMLInputElement>): void => {
|
||||
|
|
|
@ -6,6 +6,8 @@ import {connect} from 'react-redux'
|
|||
import {AutoSizer} from 'react-virtualized'
|
||||
import {withRouter, InjectedRouter} from 'react-router'
|
||||
|
||||
import {searchToFilters} from 'src/logs/utils/search'
|
||||
|
||||
import {Greys} from 'src/reusable_ui/types'
|
||||
import QueryResults from 'src/logs/components/QueryResults'
|
||||
|
||||
|
@ -21,7 +23,6 @@ import {
|
|||
setTimeMarker,
|
||||
setNamespaceAsync,
|
||||
executeQueriesAsync,
|
||||
setSearchTermAsync,
|
||||
addFilter,
|
||||
removeFilter,
|
||||
changeFilter,
|
||||
|
@ -42,6 +43,7 @@ import {getDeep} from 'src/utils/wrappers'
|
|||
import {colorForSeverity} from 'src/logs/utils/colors'
|
||||
import OverlayTechnology from 'src/reusable_ui/components/overlays/OverlayTechnology'
|
||||
import {SeverityFormatOptions, SEVERITY_SORTING_ORDER} from 'src/logs/constants'
|
||||
|
||||
import {Source, Namespace} from 'src/types'
|
||||
|
||||
import {
|
||||
|
@ -79,7 +81,6 @@ interface Props {
|
|||
setTimeMarker: (timeMarker: TimeMarker) => void
|
||||
setNamespaceAsync: (namespace: Namespace) => void
|
||||
executeQueriesAsync: () => void
|
||||
setSearchTermAsync: (searchTerm: string) => void
|
||||
setTableRelativeTime: (time: number) => void
|
||||
setTableCustomTime: (time: string) => void
|
||||
fetchOlderLogsAsync: (queryTimeEnd: string) => Promise<void>
|
||||
|
@ -94,7 +95,6 @@ interface Props {
|
|||
timeRange: TimeRange
|
||||
histogramData: HistogramData
|
||||
tableData: TableData
|
||||
searchTerm: string
|
||||
filters: Filter[]
|
||||
queryCount: number
|
||||
logConfig: LogConfig
|
||||
|
@ -186,7 +186,7 @@ class LogsPage extends Component<Props, State> {
|
|||
}
|
||||
|
||||
public render() {
|
||||
const {searchTerm, filters, queryCount, timeRange} = this.props
|
||||
const {filters, queryCount, timeRange} = this.props
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -195,10 +195,7 @@ class LogsPage extends Component<Props, State> {
|
|||
<div className="page-contents logs-viewer">
|
||||
<QueryResults count={this.histogramTotal} queryCount={queryCount} />
|
||||
<LogsGraphContainer>{this.chart}</LogsGraphContainer>
|
||||
<SearchBar
|
||||
searchString={searchTerm}
|
||||
onSearch={this.handleSubmitSearch}
|
||||
/>
|
||||
<SearchBar onSearch={this.handleSubmitSearch} />
|
||||
<FilterBar
|
||||
filters={filters || []}
|
||||
onDelete={this.handleFilterDelete}
|
||||
|
@ -528,7 +525,11 @@ class LogsPage extends Component<Props, State> {
|
|||
}
|
||||
|
||||
private handleSubmitSearch = (value: string): void => {
|
||||
this.props.setSearchTermAsync(value)
|
||||
searchToFilters(value).forEach(filter => {
|
||||
this.props.addFilter(filter)
|
||||
})
|
||||
|
||||
this.fetchNewDataset()
|
||||
}
|
||||
|
||||
private handleFilterDelete = (id: string): void => {
|
||||
|
@ -689,7 +690,6 @@ const mapStateToProps = ({
|
|||
currentNamespace,
|
||||
histogramData,
|
||||
tableData,
|
||||
searchTerm,
|
||||
filters,
|
||||
queryCount,
|
||||
logConfig,
|
||||
|
@ -704,7 +704,6 @@ const mapStateToProps = ({
|
|||
currentNamespace,
|
||||
histogramData,
|
||||
tableData,
|
||||
searchTerm,
|
||||
filters,
|
||||
queryCount,
|
||||
logConfig,
|
||||
|
@ -723,7 +722,6 @@ const mapDispatchToProps = {
|
|||
setTimeMarker,
|
||||
setNamespaceAsync,
|
||||
executeQueriesAsync,
|
||||
setSearchTermAsync,
|
||||
addFilter,
|
||||
removeFilter,
|
||||
changeFilter,
|
||||
|
|
|
@ -45,7 +45,6 @@ export const defaultState: LogsState = {
|
|||
tableQueryConfig: null,
|
||||
tableData: {columns: [], values: []},
|
||||
histogramData: [],
|
||||
searchTerm: '',
|
||||
filters: [],
|
||||
queryCount: 0,
|
||||
logConfig: {
|
||||
|
@ -221,9 +220,6 @@ export default (state: LogsState = defaultState, action: Action) => {
|
|||
backward: action.payload.data,
|
||||
},
|
||||
}
|
||||
case ActionTypes.SetSearchTerm:
|
||||
const {searchTerm} = action.payload
|
||||
return {...state, searchTerm}
|
||||
case ActionTypes.SetTableCustomTime:
|
||||
return {...state, tableTime: {custom: action.payload.time}}
|
||||
case ActionTypes.SetTableRelativeTime:
|
||||
|
|
|
@ -0,0 +1,108 @@
|
|||
import uuid from 'uuid'
|
||||
import _ from 'lodash'
|
||||
|
||||
import {Filter} from 'src/types/logs'
|
||||
import {
|
||||
Term,
|
||||
TermPart,
|
||||
TermRule,
|
||||
TermType,
|
||||
Operator,
|
||||
TokenLiteralMatch,
|
||||
} from 'src/types/logs'
|
||||
|
||||
const MESSAGE_KEY = 'message'
|
||||
|
||||
export const createRule = (
|
||||
part: TermPart,
|
||||
type: TermType = TermType.INCLUDE
|
||||
): TermRule => ({
|
||||
type,
|
||||
pattern: getPattern(type, part),
|
||||
})
|
||||
|
||||
const getPattern = (type: TermType, phrase: TermPart): RegExp => {
|
||||
switch (type) {
|
||||
case TermType.EXCLUDE:
|
||||
return new RegExp(`^${TermPart.EXCLUSION}${phrase}`)
|
||||
default:
|
||||
return new RegExp(`^${phrase}`)
|
||||
}
|
||||
}
|
||||
|
||||
export const LOG_SEARCH_TERMS: TermRule[] = [
|
||||
createRule(TermPart.SINGLE_QUOTED, TermType.EXCLUDE),
|
||||
createRule(TermPart.DOUBLE_QUOTED, TermType.EXCLUDE),
|
||||
createRule(TermPart.SINGLE_QUOTED),
|
||||
createRule(TermPart.DOUBLE_QUOTED),
|
||||
createRule(TermPart.UNQUOTED_WORD, TermType.EXCLUDE),
|
||||
createRule(TermPart.UNQUOTED_WORD),
|
||||
]
|
||||
|
||||
export const searchToFilters = (searchTerm: string): Filter[] => {
|
||||
const allTerms = extractTerms(searchTerm, LOG_SEARCH_TERMS)
|
||||
|
||||
return termsToFilters(allTerms)
|
||||
}
|
||||
|
||||
const termsToFilters = (terms: Term[]): Filter[] => {
|
||||
return terms.map(t => createMessageFilter(t.term, termToOp(t)))
|
||||
}
|
||||
|
||||
const extractTerms = (searchTerms: string, rules: TermRule[]): Term[] => {
|
||||
let tokens = []
|
||||
let text = searchTerms.trim()
|
||||
|
||||
while (!_.isEmpty(text)) {
|
||||
const {nextTerm, nextText} = extractNextTerm(text, rules)
|
||||
tokens = [...tokens, nextTerm]
|
||||
text = nextText
|
||||
}
|
||||
|
||||
return tokens
|
||||
}
|
||||
|
||||
const extractNextTerm = (text, rules: TermRule[]) => {
|
||||
const {literal, rule, nextText} = readToken(eatSpaces(text), rules)
|
||||
|
||||
const nextTerm = createTerm(rule.type, literal)
|
||||
|
||||
return {nextText, nextTerm}
|
||||
}
|
||||
|
||||
const eatSpaces = (text: string): string => {
|
||||
return text.trim()
|
||||
}
|
||||
|
||||
const readToken = (text: string, rules: TermRule[]): TokenLiteralMatch => {
|
||||
const rule = rules.find(r => text.match(new RegExp(r.pattern)) !== null)
|
||||
|
||||
const term = new RegExp(rule.pattern).exec(text)
|
||||
const literal = term[1]
|
||||
// differs from literal length because of quote and exclusion removal
|
||||
const termLength = term[0].length
|
||||
const nextText = text.slice(termLength)
|
||||
|
||||
return {literal, nextText, rule}
|
||||
}
|
||||
|
||||
const createTerm = (type: TermType, term: string): Term => ({
|
||||
type,
|
||||
term,
|
||||
})
|
||||
|
||||
const createMessageFilter = (value: string, operator: Operator): Filter => ({
|
||||
id: uuid.v4(),
|
||||
key: MESSAGE_KEY,
|
||||
value,
|
||||
operator,
|
||||
})
|
||||
|
||||
const termToOp = (term: Term): Operator => {
|
||||
switch (term.type) {
|
||||
case TermType.EXCLUDE:
|
||||
return Operator.NOT_LIKE
|
||||
case TermType.INCLUDE:
|
||||
return Operator.LIKE
|
||||
}
|
||||
}
|
|
@ -31,7 +31,6 @@ export interface LogsState {
|
|||
histogramData: object[]
|
||||
tableQueryConfig: QueryConfig | null
|
||||
tableData: TableData
|
||||
searchTerm: string | null
|
||||
filters: Filter[]
|
||||
queryCount: number
|
||||
logConfig: LogConfig
|
||||
|
@ -132,3 +131,36 @@ export interface TimeMarker {
|
|||
}
|
||||
|
||||
export type RowHeightHandler = (index: Index) => number
|
||||
|
||||
export interface Term {
|
||||
type: TermType
|
||||
term: string
|
||||
}
|
||||
|
||||
export interface TokenLiteralMatch {
|
||||
literal: string
|
||||
nextText: string
|
||||
rule: TermRule
|
||||
}
|
||||
|
||||
export interface TermRule {
|
||||
type: TermType
|
||||
pattern: RegExp
|
||||
}
|
||||
|
||||
export enum TermType {
|
||||
EXCLUDE,
|
||||
INCLUDE,
|
||||
}
|
||||
|
||||
export enum TermPart {
|
||||
EXCLUSION = '-',
|
||||
SINGLE_QUOTED = "'([^']+)'",
|
||||
DOUBLE_QUOTED = '"([^"]+)"',
|
||||
UNQUOTED_WORD = '([\\S]+)',
|
||||
}
|
||||
|
||||
export enum Operator {
|
||||
NOT_LIKE = '!~',
|
||||
LIKE = '=~',
|
||||
}
|
||||
|
|
|
@ -0,0 +1,158 @@
|
|||
import {searchToFilters} from 'src/logs/utils/search'
|
||||
import {Operator} from 'src/types/logs'
|
||||
|
||||
describe('Logs.searchToFilters', () => {
|
||||
const isUUID = expect.stringMatching(
|
||||
/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/
|
||||
)
|
||||
|
||||
it('can return like filters for terms', () => {
|
||||
const text = 'seq_!@.#: TERMS /api/search'
|
||||
const actual = searchToFilters(text)
|
||||
|
||||
const expected = [
|
||||
{
|
||||
id: isUUID,
|
||||
key: 'message',
|
||||
value: 'seq_!@.#:',
|
||||
operator: Operator.LIKE,
|
||||
},
|
||||
{
|
||||
id: isUUID,
|
||||
key: 'message',
|
||||
value: 'TERMS',
|
||||
operator: Operator.LIKE,
|
||||
},
|
||||
{
|
||||
id: isUUID,
|
||||
key: 'message',
|
||||
value: '/api/search',
|
||||
operator: Operator.LIKE,
|
||||
},
|
||||
]
|
||||
|
||||
expect(actual).toEqual(expected)
|
||||
})
|
||||
|
||||
it('can return not like filters for term exclusions', () => {
|
||||
const text = '/api/search -status_bad -@123!'
|
||||
const actual = searchToFilters(text)
|
||||
|
||||
const expected = [
|
||||
{
|
||||
id: isUUID,
|
||||
key: 'message',
|
||||
value: '/api/search',
|
||||
operator: Operator.LIKE,
|
||||
},
|
||||
{
|
||||
id: isUUID,
|
||||
key: 'message',
|
||||
value: 'status_bad',
|
||||
operator: Operator.NOT_LIKE,
|
||||
},
|
||||
{
|
||||
id: isUUID,
|
||||
key: 'message',
|
||||
value: '@123!',
|
||||
operator: Operator.NOT_LIKE,
|
||||
},
|
||||
]
|
||||
|
||||
expect(actual).toEqual(expected)
|
||||
})
|
||||
|
||||
it('can create filters for phrases', () => {
|
||||
const text = '"/api/search status:200" "a success"'
|
||||
const actual = searchToFilters(text)
|
||||
|
||||
const expected = [
|
||||
{
|
||||
id: isUUID,
|
||||
key: 'message',
|
||||
value: '/api/search status:200',
|
||||
operator: Operator.LIKE,
|
||||
},
|
||||
{
|
||||
id: isUUID,
|
||||
key: 'message',
|
||||
value: 'a success',
|
||||
operator: Operator.LIKE,
|
||||
},
|
||||
]
|
||||
|
||||
expect(actual).toEqual(expected)
|
||||
})
|
||||
|
||||
it('can create filters for excluded phrases', () => {
|
||||
const text = '-"/api/search status:200" -"a success"'
|
||||
const actual = searchToFilters(text)
|
||||
|
||||
const expected = [
|
||||
{
|
||||
id: isUUID,
|
||||
key: 'message',
|
||||
value: '/api/search status:200',
|
||||
operator: Operator.NOT_LIKE,
|
||||
},
|
||||
{
|
||||
id: isUUID,
|
||||
key: 'message',
|
||||
value: 'a success',
|
||||
operator: Operator.NOT_LIKE,
|
||||
},
|
||||
]
|
||||
|
||||
expect(actual).toEqual(expected)
|
||||
})
|
||||
|
||||
it('can create filters for phrases and terms', () => {
|
||||
const text = `status:4\d{2} -"NOT FOUND" 'some "quote"' -thing`
|
||||
const actual = searchToFilters(text)
|
||||
|
||||
const expected = [
|
||||
{
|
||||
id: isUUID,
|
||||
key: 'message',
|
||||
value: 'status:4d{2}',
|
||||
operator: Operator.LIKE,
|
||||
},
|
||||
{
|
||||
id: isUUID,
|
||||
key: 'message',
|
||||
value: 'NOT FOUND',
|
||||
operator: Operator.NOT_LIKE,
|
||||
},
|
||||
{
|
||||
id: isUUID,
|
||||
key: 'message',
|
||||
value: 'some "quote"',
|
||||
operator: Operator.LIKE,
|
||||
},
|
||||
{
|
||||
id: isUUID,
|
||||
key: 'message',
|
||||
value: 'thing',
|
||||
operator: Operator.NOT_LIKE,
|
||||
},
|
||||
]
|
||||
|
||||
expect(actual).toEqual(expected)
|
||||
})
|
||||
|
||||
it('can return quoted phrase containing single quotes', () => {
|
||||
const text = `"some 'quote'"`
|
||||
const actual = searchToFilters(text)
|
||||
|
||||
const expected = [
|
||||
{
|
||||
id: isUUID,
|
||||
key: 'message',
|
||||
value: "some 'quote'",
|
||||
operator: Operator.LIKE,
|
||||
},
|
||||
]
|
||||
|
||||
expect(actual).toEqual(expected)
|
||||
})
|
||||
})
|
Loading…
Reference in New Issue