diff --git a/ui/src/logs/actions/index.ts b/ui/src/logs/actions/index.ts index b752bd7280..9eeec47065 100644 --- a/ui/src/logs/actions/index.ts +++ b/ui/src/logs/actions/index.ts @@ -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(state, 'logs.timeRange', null) @@ -134,6 +147,9 @@ const getHistogramQueryConfig = (state: State): QueryConfig | null => const getTableQueryConfig = (state: State): QueryConfig | null => getDeep(state, 'logs.tableQueryConfig', null) +const getSearchTerm = (state: State): string | null => + getDeep(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 diff --git a/ui/src/logs/components/LogsSearchBar.tsx b/ui/src/logs/components/LogsSearchBar.tsx index fdfa233357..42dc56fb46 100644 --- a/ui/src/logs/components/LogsSearchBar.tsx +++ b/ui/src/logs/components/LogsSearchBar.tsx @@ -2,13 +2,24 @@ import React, {PureComponent, ChangeEvent, KeyboardEvent} from 'react' interface Props { searchString: string - onChange: (e: ChangeEvent) => void - onSearch: () => void + onSearch: (value: string) => void } -class LogsSearchBar extends PureComponent { +interface State { + searchTerm: string +} + +class LogsSearchBar extends PureComponent { + constructor(props: Props) { + super(props) + + this.state = { + searchTerm: props.searchString, + } + } + public render() { - const {searchString, onSearch, onChange} = this.props + const {searchTerm} = this.state return (
@@ -16,8 +27,8 @@ class LogsSearchBar extends PureComponent { { />
- @@ -33,11 +44,19 @@ class LogsSearchBar extends PureComponent { ) } + private handleSearch = () => { + this.props.onSearch(this.state.searchTerm) + } + private handleInputKeyDown = (e: KeyboardEvent): void => { if (e.key === 'Enter') { - return this.props.onSearch() + return this.handleSearch() } } + + private handleChange = (e: ChangeEvent): void => { + this.setState({searchTerm: e.target.value}) + } } export default LogsSearchBar diff --git a/ui/src/logs/components/LogsTable.tsx b/ui/src/logs/components/LogsTable.tsx index d5ee98218c..7bb4b20c48 100644 --- a/ui/src/logs/components/LogsTable.tsx +++ b/ui/src/logs/components/LogsTable.tsx @@ -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 { +class LogsTable extends PureComponent { + 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 { 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 { 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 { ) } - 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 { 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 { return getDeep( { 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(FACILITY_CODES, key, '') - } - private headerRenderer = ({key, style, columnIndex}) => { const value = getDeep( this.props, @@ -159,12 +155,18 @@ class LogsTable extends PureComponent { 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 ( +
+
+
+ ) } return ( diff --git a/ui/src/logs/containers/LogsPage.tsx b/ui/src/logs/containers/LogsPage.tsx index 9a36fb118d..2770264971 100644 --- a/ui/src/logs/containers/LogsPage.tsx +++ b/ui/src/logs/containers/LogsPage.tsx @@ -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 { } 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 {
{this.chart} { ) } - private handleSearchInputChange = ( - e: ChangeEvent - ): 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) diff --git a/ui/src/logs/reducers/index.ts b/ui/src/logs/reducers/index.ts index 96484f0084..417f4472e2 100644 --- a/ui/src/logs/reducers/index.ts +++ b/ui/src/logs/reducers/index.ts @@ -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 } diff --git a/ui/src/logs/utils/index.ts b/ui/src/logs/utils/index.ts index 270e591e71..c06d08d8f1 100644 --- a/ui/src/logs/utils/index.ts +++ b/ui/src/logs/utils/index.ts @@ -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 diff --git a/ui/src/style/pages/logs-viewer.scss b/ui/src/style/pages/logs-viewer.scss index 18febbbedb..db62bbb2dd 100644 --- a/ui/src/style/pages/logs-viewer.scss +++ b/ui/src/style/pages/logs-viewer.scss @@ -201,4 +201,37 @@ $logs-viewer-gutter: 60px; width: 100%; 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); + } } \ No newline at end of file diff --git a/ui/src/types/localStorage.ts b/ui/src/types/localStorage.ts index 2b328b9481..5400c455e1 100644 --- a/ui/src/types/localStorage.ts +++ b/ui/src/types/localStorage.ts @@ -9,6 +9,7 @@ export interface LogsState { histogramData: object[] tableQueryConfig: QueryConfig | null tableData: object[] + searchTerm: string | null } export interface LocalStorage { diff --git a/ui/src/utils/influxql.ts b/ui/src/utils/influxql.ts index 083c72cde4..40488500c9 100644 --- a/ui/src/utils/influxql.ts +++ b/ui/src/utils/influxql.ts @@ -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})` }