Merge pull request #4163 from influxdata/enhancement/log-search-operators

Enhance log search params
pull/4192/head
Delmer 2018-08-13 16:06:24 -04:00 committed by GitHub
commit 98c28beab8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 311 additions and 36 deletions

View File

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

View File

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

View File

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

View File

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

108
ui/src/logs/utils/search.ts Normal file
View File

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

View File

@ -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 = '=~',
}

View File

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