Merge pull request #3543 from influxdata/log-viewer/basic-search
Implements basic log viewer searchpull/10616/head
commit
03f7d277ba
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
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() {
|
||||
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
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -9,6 +9,7 @@ export interface LogsState {
|
|||
histogramData: object[]
|
||||
tableQueryConfig: QueryConfig | null
|
||||
tableData: object[]
|
||||
searchTerm: string | null
|
||||
}
|
||||
|
||||
export interface LocalStorage {
|
||||
|
|
|
@ -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})`
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue