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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,15 @@
import _ from 'lodash'
import moment from 'moment'
import uuid from 'uuid'
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
@ -9,7 +18,7 @@ const histogramFields = [
alias: '',
args: [
{
alias: '',
alias: 'message',
type: 'field',
value: 'message',
},
@ -20,15 +29,25 @@ const histogramFields = [
]
const tableFields = [
{
alias: 'severity',
type: 'field',
value: 'severity',
},
{
alias: 'timestamp',
type: 'field',
value: 'timestamp',
},
{
alias: 'facility_code',
alias: 'severity_text',
type: 'field',
value: 'facility_code',
value: 'severity',
},
{
alias: 'facility',
type: 'field',
value: 'facility',
},
{
alias: 'procid',
@ -36,9 +55,14 @@ const tableFields = [
value: 'procid',
},
{
alias: 'severity_code',
alias: 'appname',
type: 'field',
value: 'severity_code',
value: 'appname',
},
{
alias: 'host',
type: 'field',
value: 'host',
},
{
alias: 'message',
@ -56,6 +80,25 @@ const defaultQueryConfig = {
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 {upper, lower, seconds} = range

View File

@ -202,3 +202,36 @@ $logs-viewer-gutter: 60px;
height: 100%;
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[]
tableQueryConfig: QueryConfig | null
tableData: object[]
searchTerm: string | null
}
export interface LocalStorage {

View File

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