Merge pull request #3543 from influxdata/log-viewer/basic-search

Implements basic log viewer search
pull/10616/head
Brandon Farmer 2018-05-31 14:03:59 -07:00 committed by GitHub
commit 03f7d277ba
9 changed files with 212 additions and 84 deletions

View File

@ -2,7 +2,11 @@ import _ from 'lodash'
import {Source, Namespace, TimeRange, QueryConfig} from 'src/types' import {Source, Namespace, TimeRange, QueryConfig} from 'src/types'
import {getSource} from 'src/shared/apis' import {getSource} from 'src/shared/apis'
import {getDatabasesWithRetentionPolicies} from 'src/shared/apis/databases' import {getDatabasesWithRetentionPolicies} from 'src/shared/apis/databases'
import {buildHistogramQueryConfig, buildTableQueryConfig} from 'src/logs/utils' import {
buildHistogramQueryConfig,
buildTableQueryConfig,
buildLogQuery,
} from 'src/logs/utils'
import {getDeep} from 'src/utils/wrappers' import {getDeep} from 'src/utils/wrappers'
import buildQuery from 'src/utils/influxql' import buildQuery from 'src/utils/influxql'
import {executeQueryAsync} from 'src/logs/api' import {executeQueryAsync} from 'src/logs/api'
@ -42,6 +46,7 @@ export enum ActionTypes {
SetTableQueryConfig = 'LOGS_SET_TABLE_QUERY_CONFIG', SetTableQueryConfig = 'LOGS_SET_TABLE_QUERY_CONFIG',
SetTableData = 'LOGS_SET_TABLE_DATA', SetTableData = 'LOGS_SET_TABLE_DATA',
ChangeZoom = 'LOGS_CHANGE_ZOOM', ChangeZoom = 'LOGS_CHANGE_ZOOM',
SetSearchTerm = 'LOGS_SET_SEARCH_TERM',
} }
interface SetSourceAction { interface SetSourceAction {
@ -100,6 +105,13 @@ interface SetTableData {
} }
} }
interface SetSearchTerm {
type: ActionTypes.SetSearchTerm
payload: {
searchTerm: string
}
}
interface ChangeZoomAction { interface ChangeZoomAction {
type: ActionTypes.ChangeZoom type: ActionTypes.ChangeZoom
payload: { payload: {
@ -118,6 +130,7 @@ export type Action =
| ChangeZoomAction | ChangeZoomAction
| SetTableData | SetTableData
| SetTableQueryConfig | SetTableQueryConfig
| SetSearchTerm
const getTimeRange = (state: State): TimeRange | null => const getTimeRange = (state: State): TimeRange | null =>
getDeep<TimeRange | null>(state, 'logs.timeRange', null) getDeep<TimeRange | null>(state, 'logs.timeRange', null)
@ -134,6 +147,9 @@ const getHistogramQueryConfig = (state: State): QueryConfig | null =>
const getTableQueryConfig = (state: State): QueryConfig | null => const getTableQueryConfig = (state: State): QueryConfig | null =>
getDeep<QueryConfig | null>(state, 'logs.tableQueryConfig', null) getDeep<QueryConfig | null>(state, 'logs.tableQueryConfig', null)
const getSearchTerm = (state: State): string | null =>
getDeep<string | null>(state, 'logs.searchTerm', null)
export const setSource = (source: Source): SetSourceAction => ({ export const setSource = (source: Source): SetSourceAction => ({
type: ActionTypes.SetSource, type: ActionTypes.SetSource,
payload: {source}, payload: {source},
@ -154,9 +170,10 @@ export const executeHistogramQueryAsync = () => async (
const timeRange = getTimeRange(state) const timeRange = getTimeRange(state)
const namespace = getNamespace(state) const namespace = getNamespace(state)
const proxyLink = getProxyLink(state) const proxyLink = getProxyLink(state)
const searchTerm = getSearchTerm(state)
if (_.every([queryConfig, timeRange, namespace, proxyLink])) { if (_.every([queryConfig, timeRange, namespace, proxyLink])) {
const query = buildQuery(timeRange, queryConfig) const query = buildLogQuery(timeRange, queryConfig, searchTerm)
const response = await executeQueryAsync(proxyLink, namespace, query) const response = await executeQueryAsync(proxyLink, namespace, query)
dispatch(setHistogramData(response)) dispatch(setHistogramData(response))
@ -178,9 +195,10 @@ export const executeTableQueryAsync = () => async (
const timeRange = getTimeRange(state) const timeRange = getTimeRange(state)
const namespace = getNamespace(state) const namespace = getNamespace(state)
const proxyLink = getProxyLink(state) const proxyLink = getProxyLink(state)
const searchTerm = getSearchTerm(state)
if (_.every([queryConfig, timeRange, namespace, proxyLink])) { if (_.every([queryConfig, timeRange, namespace, proxyLink])) {
const query = buildQuery(timeRange, queryConfig) const query = buildLogQuery(timeRange, queryConfig, searchTerm)
const response = await executeQueryAsync(proxyLink, namespace, query) const response = await executeQueryAsync(proxyLink, namespace, query)
const series = getDeep(response, 'results.0.series.0', defaultTableData) const series = getDeep(response, 'results.0.series.0', defaultTableData)
@ -194,6 +212,14 @@ export const executeQueriesAsync = () => async dispatch => {
dispatch(executeTableQueryAsync()) dispatch(executeTableQueryAsync())
} }
export const setSearchTermAsync = (searchTerm: string) => async dispatch => {
dispatch({
type: ActionTypes.SetSearchTerm,
payload: {searchTerm},
})
dispatch(executeQueriesAsync())
}
export const setHistogramQueryConfigAsync = () => async ( export const setHistogramQueryConfigAsync = () => async (
dispatch, dispatch,
getState: GetState getState: GetState

View File

@ -2,13 +2,24 @@ import React, {PureComponent, ChangeEvent, KeyboardEvent} from 'react'
interface Props { interface Props {
searchString: string searchString: string
onChange: (e: ChangeEvent<HTMLInputElement>) => void onSearch: (value: string) => void
onSearch: () => void
} }
class LogsSearchBar extends PureComponent<Props> { interface State {
searchTerm: string
}
class LogsSearchBar extends PureComponent<Props, State> {
constructor(props: Props) {
super(props)
this.state = {
searchTerm: props.searchString,
}
}
public render() { public render() {
const {searchString, onSearch, onChange} = this.props const {searchTerm} = this.state
return ( return (
<div className="logs-viewer--search-bar"> <div className="logs-viewer--search-bar">
@ -16,8 +27,8 @@ class LogsSearchBar extends PureComponent<Props> {
<input <input
type="text" type="text"
placeholder="Search logs using Keywords or Regular Expressions..." placeholder="Search logs using Keywords or Regular Expressions..."
value={searchString} value={searchTerm}
onChange={onChange} onChange={this.handleChange}
onKeyDown={this.handleInputKeyDown} onKeyDown={this.handleInputKeyDown}
className="form-control input-sm" className="form-control input-sm"
spellCheck={false} spellCheck={false}
@ -25,7 +36,7 @@ class LogsSearchBar extends PureComponent<Props> {
/> />
<span className="icon search" /> <span className="icon search" />
</div> </div>
<button className="btn btn-sm btn-primary" onClick={onSearch}> <button className="btn btn-sm btn-primary" onClick={this.handleSearch}>
<span className="icon search" /> <span className="icon search" />
Search Search
</button> </button>
@ -33,11 +44,19 @@ class LogsSearchBar extends PureComponent<Props> {
) )
} }
private handleSearch = () => {
this.props.onSearch(this.state.searchTerm)
}
private handleInputKeyDown = (e: KeyboardEvent<HTMLInputElement>): void => { private handleInputKeyDown = (e: KeyboardEvent<HTMLInputElement>): void => {
if (e.key === 'Enter') { if (e.key === 'Enter') {
return this.props.onSearch() return this.handleSearch()
} }
} }
private handleChange = (e: ChangeEvent<HTMLInputElement>): void => {
this.setState({searchTerm: e.target.value})
}
} }
export default LogsSearchBar export default LogsSearchBar

View File

@ -1,3 +1,4 @@
import _ from 'lodash'
import moment from 'moment' import moment from 'moment'
import React, {PureComponent} from 'react' import React, {PureComponent} from 'react'
import {Grid, AutoSizer} from 'react-virtualized' import {Grid, AutoSizer} from 'react-virtualized'
@ -11,34 +12,18 @@ interface Props {
} }
} }
const FACILITY_CODES = [ interface State {
'kern', scrollLeft: number
'user', }
'mail',
'daemon',
'auth',
'syslog',
'lpr',
'news',
'uucp',
'clock',
'authpriv',
'ftp',
'NTP',
'log audit',
'log alert',
'cron',
'local0',
'local1',
'local2',
'local3',
'local4',
'local5',
'local6',
'local7',
]
class LogsTable extends PureComponent<Props> { class LogsTable extends PureComponent<Props, State> {
constructor(props: Props) {
super(props)
this.state = {
scrollLeft: 0,
}
}
public render() { public render() {
const rowCount = getDeep(this.props, 'data.values.length', 0) const rowCount = getDeep(this.props, 'data.values.length', 0)
const columnCount = getDeep(this.props, 'data.columns.length', 1) - 1 const columnCount = getDeep(this.props, 'data.columns.length', 1) - 1
@ -52,6 +37,8 @@ class LogsTable extends PureComponent<Props> {
rowHeight={40} rowHeight={40}
rowCount={1} rowCount={1}
width={width} width={width}
scrollLeft={this.state.scrollLeft}
onScroll={this.handleScroll}
cellRenderer={this.headerRenderer} cellRenderer={this.headerRenderer}
columnCount={columnCount} columnCount={columnCount}
columnWidth={this.getColumnWidth} columnWidth={this.getColumnWidth}
@ -69,6 +56,8 @@ class LogsTable extends PureComponent<Props> {
rowHeight={40} rowHeight={40}
rowCount={rowCount} rowCount={rowCount}
width={width} width={width}
scrollLeft={this.state.scrollLeft}
onScroll={this.handleScroll}
cellRenderer={this.cellRenderer} cellRenderer={this.cellRenderer}
columnCount={columnCount} columnCount={columnCount}
columnWidth={this.getColumnWidth} columnWidth={this.getColumnWidth}
@ -80,24 +69,26 @@ class LogsTable extends PureComponent<Props> {
) )
} }
private severityLevel(value: number): string { private handleScroll = scrollInfo => {
const {scrollLeft} = scrollInfo
this.setState({scrollLeft})
}
private severityLevel(value: string): string {
switch (value) { switch (value) {
case 0: case 'emerg':
return 'Emergency' return 'Emergency'
case 1: case 'alert':
return 'Alert' return 'Alert'
case 2: case 'crit':
return 'Critical' return 'Critical'
case 3: case 'err':
return 'Error' return 'Error'
case 4: case 'info':
return 'Warning'
case 5:
return 'Notice'
case 6:
return 'Informational' return 'Informational'
default: default:
return 'Debug' return _.capitalize(value)
} }
} }
@ -106,9 +97,17 @@ class LogsTable extends PureComponent<Props> {
switch (column) { switch (column) {
case 'message': case 'message':
return 700 return 900
case 'timestamp': case 'timestamp':
return 400 return 200
case 'procid':
return 100
case 'facility':
return 150
case 'severity_1':
return 150
case 'severity':
return 24
default: default:
return 200 return 200
} }
@ -118,20 +117,17 @@ class LogsTable extends PureComponent<Props> {
return getDeep<string>( return getDeep<string>(
{ {
timestamp: 'Timestamp', timestamp: 'Timestamp',
facility_code: 'Facility',
procid: 'Proc ID', procid: 'Proc ID',
severity_code: 'Severity',
message: 'Message', message: 'Message',
appname: 'Application',
severity: '',
severity_1: 'Severity',
}, },
key, key,
'' _.capitalize(key)
) )
} }
private facility(key: number): string {
return getDeep<string>(FACILITY_CODES, key, '')
}
private headerRenderer = ({key, style, columnIndex}) => { private headerRenderer = ({key, style, columnIndex}) => {
const value = getDeep<string>( const value = getDeep<string>(
this.props, this.props,
@ -159,12 +155,18 @@ class LogsTable extends PureComponent<Props> {
case 'timestamp': case 'timestamp':
value = moment(+value / 1000000).format('YYYY/MM/DD HH:mm:ss') value = moment(+value / 1000000).format('YYYY/MM/DD HH:mm:ss')
break break
case 'severity_code': case 'severity_1':
value = this.severityLevel(+value) value = this.severityLevel(value)
break
case 'facility_code':
value = this.facility(+value)
break break
case 'severity':
return (
<div style={style} key={key}>
<div
className={`logs-viewer--dot ${value}-severity`}
title={this.severityLevel(value)}
/>
</div>
)
} }
return ( return (

View File

@ -1,4 +1,4 @@
import React, {PureComponent, ChangeEvent} from 'react' import React, {PureComponent} from 'react'
import {connect} from 'react-redux' import {connect} from 'react-redux'
import { import {
getSourceAndPopulateNamespacesAsync, getSourceAndPopulateNamespacesAsync,
@ -6,6 +6,7 @@ import {
setNamespaceAsync, setNamespaceAsync,
executeQueriesAsync, executeQueriesAsync,
changeZoomAsync, changeZoomAsync,
setSearchTermAsync,
} from 'src/logs/actions' } from 'src/logs/actions'
import {getSourcesAsync} from 'src/shared/actions/sources' import {getSourcesAsync} from 'src/shared/actions/sources'
import LogViewerHeader from 'src/logs/components/LogViewerHeader' import LogViewerHeader from 'src/logs/components/LogViewerHeader'
@ -37,12 +38,14 @@ interface Props {
setNamespaceAsync: (namespace: Namespace) => void setNamespaceAsync: (namespace: Namespace) => void
changeZoomAsync: (timeRange: TimeRange) => void changeZoomAsync: (timeRange: TimeRange) => void
executeQueriesAsync: () => void executeQueriesAsync: () => void
setSearchTermAsync: (searchTerm: string) => void
timeRange: TimeRange timeRange: TimeRange
histogramData: object[] histogramData: object[]
tableData: { tableData: {
columns: string[] columns: string[]
values: string[] values: string[]
} }
searchTerm: string
} }
interface State { interface State {
@ -85,7 +88,8 @@ class LogsPage extends PureComponent<Props, State> {
} }
public render() { public render() {
const {searchString, filters} = this.state const {filters} = this.state
const {searchTerm} = this.props
const count = getDeep(this.props, 'tableData.values.length', 0) const count = getDeep(this.props, 'tableData.values.length', 0)
@ -95,8 +99,7 @@ class LogsPage extends PureComponent<Props, State> {
<div className="page-contents logs-viewer"> <div className="page-contents logs-viewer">
<Graph>{this.chart}</Graph> <Graph>{this.chart}</Graph>
<SearchBar <SearchBar
searchString={searchString} searchString={searchTerm}
onChange={this.handleSearchInputChange}
onSearch={this.handleSubmitSearch} onSearch={this.handleSubmitSearch}
/> />
<FilterBar <FilterBar
@ -144,14 +147,8 @@ class LogsPage extends PureComponent<Props, State> {
) )
} }
private handleSearchInputChange = ( private handleSubmitSearch = (value: string): void => {
e: ChangeEvent<HTMLInputElement> this.props.setSearchTermAsync(value)
): void => {
this.setState({searchString: e.target.value})
}
private handleSubmitSearch = (): void => {
// do the thing
} }
private handleUpdateFilters = (filters: Filter[]): void => { private handleUpdateFilters = (filters: Filter[]): void => {
@ -187,6 +184,7 @@ const mapStateToProps = ({
currentNamespace, currentNamespace,
histogramData, histogramData,
tableData, tableData,
searchTerm,
}, },
}) => ({ }) => ({
sources, sources,
@ -196,6 +194,7 @@ const mapStateToProps = ({
currentNamespace, currentNamespace,
histogramData, histogramData,
tableData, tableData,
searchTerm,
}) })
const mapDispatchToProps = { const mapDispatchToProps = {
@ -205,6 +204,7 @@ const mapDispatchToProps = {
setNamespaceAsync, setNamespaceAsync,
executeQueriesAsync, executeQueriesAsync,
changeZoomAsync, changeZoomAsync,
setSearchTermAsync,
} }
export default connect(mapStateToProps, mapDispatchToProps)(LogsPage) export default connect(mapStateToProps, mapDispatchToProps)(LogsPage)

View File

@ -10,6 +10,7 @@ const defaultState: LogsState = {
tableQueryConfig: null, tableQueryConfig: null,
tableData: [], tableData: [],
histogramData: [], histogramData: [],
searchTerm: null,
} }
export default (state: LogsState = defaultState, action: Action) => { export default (state: LogsState = defaultState, action: Action) => {
@ -33,6 +34,9 @@ export default (state: LogsState = defaultState, action: Action) => {
case ActionTypes.ChangeZoom: case ActionTypes.ChangeZoom:
const {timeRange, data} = action.payload const {timeRange, data} = action.payload
return {...state, timeRange, histogramData: data} return {...state, timeRange, histogramData: data}
case ActionTypes.SetSearchTerm:
const {searchTerm} = action.payload
return {...state, searchTerm}
default: default:
return state return state
} }

View File

@ -1,6 +1,15 @@
import _ from 'lodash'
import moment from 'moment' import moment from 'moment'
import uuid from 'uuid' import uuid from 'uuid'
import {TimeRange, Namespace, QueryConfig} from 'src/types' import {TimeRange, Namespace, QueryConfig} from 'src/types'
import {NULL_STRING} from 'src/shared/constants/queryFillOptions'
import {
quoteIfTimestamp,
buildSelect,
buildWhereClause,
buildGroupBy,
buildFill,
} from 'src/utils/influxql'
const BIN_COUNT = 30 const BIN_COUNT = 30
@ -9,7 +18,7 @@ const histogramFields = [
alias: '', alias: '',
args: [ args: [
{ {
alias: '', alias: 'message',
type: 'field', type: 'field',
value: 'message', value: 'message',
}, },
@ -20,15 +29,25 @@ const histogramFields = [
] ]
const tableFields = [ const tableFields = [
{
alias: 'severity',
type: 'field',
value: 'severity',
},
{ {
alias: 'timestamp', alias: 'timestamp',
type: 'field', type: 'field',
value: 'timestamp', value: 'timestamp',
}, },
{ {
alias: 'facility_code', alias: 'severity_text',
type: 'field', type: 'field',
value: 'facility_code', value: 'severity',
},
{
alias: 'facility',
type: 'field',
value: 'facility',
}, },
{ {
alias: 'procid', alias: 'procid',
@ -36,9 +55,14 @@ const tableFields = [
value: 'procid', value: 'procid',
}, },
{ {
alias: 'severity_code', alias: 'appname',
type: 'field', type: 'field',
value: 'severity_code', value: 'appname',
},
{
alias: 'host',
type: 'field',
value: 'host',
}, },
{ {
alias: 'message', alias: 'message',
@ -56,6 +80,25 @@ const defaultQueryConfig = {
tags: {}, tags: {},
} }
export function buildLogQuery(
timeRange: TimeRange,
config: QueryConfig,
searchTerm: string | null = null
): string {
const {groupBy, fill = NULL_STRING, tags, areTagsAccepted} = config
const {upper, lower} = quoteIfTimestamp(timeRange)
const select = buildSelect(config, '')
const dimensions = buildGroupBy(groupBy)
const fillClause = groupBy.time ? buildFill(fill) : ''
let condition = buildWhereClause({lower, upper, tags, areTagsAccepted})
if (!_.isEmpty(searchTerm)) {
condition = `${condition} AND message =~ ${new RegExp(searchTerm)}`
}
return `${select}${condition}${dimensions}${fillClause}`
}
const computeSeconds = (range: TimeRange) => { const computeSeconds = (range: TimeRange) => {
const {upper, lower, seconds} = range const {upper, lower, seconds} = range

View File

@ -201,4 +201,37 @@ $logs-viewer-gutter: 60px;
width: 100%; width: 100%;
height: 100%; height: 100%;
padding: 8px 16px; padding: 8px 16px;
}
.logs-viewer--dot {
width: 14px;
height: 14px;
border-radius: 50%;
display: inline-block;
background-color: $g0-obsidian;
&.emerg-severity {
@include gradient-diag-up($c-ruby, $c-fire);
}
&.alert-severity {
@include gradient-diag-up($c-fire, $c-curacao);
}
&.crit-severity {
@include gradient-diag-up($c-curacao, $c-tiger);
}
&.err-severity {
@include gradient-diag-up($c-tiger, $c-pineapple);
}
&.warning-severity {
@include gradient-diag-up($c-pineapple, $c-thunder);
}
&.notice-severity {
@include gradient-diag-up($c-rainforest, $c-honeydew);
}
&.info-severity {
@include gradient-diag-up($c-star, $c-comet);
}
&.debug-severity {
@include gradient-diag-up($g5-pepper, $g6-smoke);
}
} }

View File

@ -9,6 +9,7 @@ export interface LogsState {
histogramData: object[] histogramData: object[]
tableQueryConfig: QueryConfig | null tableQueryConfig: QueryConfig | null
tableData: object[] tableData: object[]
searchTerm: string | null
} }
export interface LocalStorage { export interface LocalStorage {

View File

@ -42,7 +42,7 @@ export default function buildInfluxQLQuery(
return `${select}${condition}${dimensions}${fillClause}` return `${select}${condition}${dimensions}${fillClause}`
} }
function buildSelect( export function buildSelect(
{fields, database, retentionPolicy, measurement}: QueryConfig, {fields, database, retentionPolicy, measurement}: QueryConfig,
shift: string | null = null shift: string | null = null
): string { ): string {
@ -122,7 +122,7 @@ function buildFields(fieldFuncs: Field[], shift = ''): string {
.join(', ') .join(', ')
} }
function buildWhereClause({ export function buildWhereClause({
lower, lower,
upper, upper,
tags, tags,
@ -163,7 +163,7 @@ function buildWhereClause({
return ` WHERE ${subClauses.join(' AND ')}` return ` WHERE ${subClauses.join(' AND ')}`
} }
function buildGroupBy(groupBy: GroupBy): string { export function buildGroupBy(groupBy: GroupBy): string {
return `${buildGroupByTime(groupBy)}${buildGroupByTags(groupBy)}` return `${buildGroupByTime(groupBy)}${buildGroupByTags(groupBy)}`
} }
@ -191,7 +191,7 @@ function buildGroupByTags(groupBy: GroupBy): string {
return ` GROUP BY ${tags}` return ` GROUP BY ${tags}`
} }
function buildFill(fill: string): string { export function buildFill(fill: string): string {
return ` FILL(${fill})` return ` FILL(${fill})`
} }