diff --git a/ui/src/localStorage.ts b/ui/src/localStorage.ts index e6688122a..dd36a5580 100644 --- a/ui/src/localStorage.ts +++ b/ui/src/localStorage.ts @@ -63,6 +63,8 @@ export const saveToLocalStorage = ({ const appPersisted = {app: {persisted}} const dashTimeV1 = {ranges: normalizer(ranges)} + const minimalLogs = _.omit(logs, ['tableData', 'histogramData']) + window.localStorage.setItem( 'state', JSON.stringify({ @@ -73,7 +75,7 @@ export const saveToLocalStorage = ({ dataExplorer, dataExplorerQueryConfigs, script, - logs, + logs: {...minimalLogs, histogramData: [], tableData: {}}, }) ) } catch (err) { diff --git a/ui/src/logs/actions/index.ts b/ui/src/logs/actions/index.ts index d59f286cd..b752bd728 100644 --- a/ui/src/logs/actions/index.ts +++ b/ui/src/logs/actions/index.ts @@ -1,13 +1,36 @@ +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} from 'src/logs/utils' +import {buildHistogramQueryConfig, buildTableQueryConfig} from 'src/logs/utils' import {getDeep} from 'src/utils/wrappers' import buildQuery from 'src/utils/influxql' import {executeQueryAsync} from 'src/logs/api' import {LogsState} from 'src/types/localStorage' -type GetState = () => {logs: LogsState} +interface TableData { + columns: string[] + values: string[] +} + +const defaultTableData = { + columns: [ + 'time', + 'message', + 'facility_code', + 'procid', + 'severity_code', + 'timestamp', + 'version', + ], + values: [], +} + +interface State { + logs: LogsState +} + +type GetState = () => State export enum ActionTypes { SetSource = 'LOGS_SET_SOURCE', @@ -16,6 +39,8 @@ export enum ActionTypes { SetNamespace = 'LOGS_SET_NAMESPACE', SetHistogramQueryConfig = 'LOGS_SET_HISTOGRAM_QUERY_CONFIG', SetHistogramData = 'LOGS_SET_HISTOGRAM_DATA', + SetTableQueryConfig = 'LOGS_SET_TABLE_QUERY_CONFIG', + SetTableData = 'LOGS_SET_TABLE_DATA', ChangeZoom = 'LOGS_CHANGE_ZOOM', } @@ -61,6 +86,20 @@ interface SetHistogramData { } } +interface SetTableQueryConfig { + type: ActionTypes.SetTableQueryConfig + payload: { + queryConfig: QueryConfig + } +} + +interface SetTableData { + type: ActionTypes.SetTableData + payload: { + data: object + } +} + interface ChangeZoomAction { type: ActionTypes.ChangeZoom payload: { @@ -77,12 +116,32 @@ export type Action = | SetHistogramQueryConfig | SetHistogramData | ChangeZoomAction + | SetTableData + | SetTableQueryConfig + +const getTimeRange = (state: State): TimeRange | null => + getDeep(state, 'logs.timeRange', null) + +const getNamespace = (state: State): Namespace | null => + getDeep(state, 'logs.currentNamespace', null) + +const getProxyLink = (state: State): string | null => + getDeep(state, 'logs.currentSource.links.proxy', null) + +const getHistogramQueryConfig = (state: State): QueryConfig | null => + getDeep(state, 'logs.histogramQueryConfig', null) + +const getTableQueryConfig = (state: State): QueryConfig | null => + getDeep(state, 'logs.tableQueryConfig', null) export const setSource = (source: Source): SetSourceAction => ({ type: ActionTypes.SetSource, - payload: { - source, - }, + payload: {source}, +}) + +const setHistogramData = (response): SetHistogramData => ({ + type: ActionTypes.SetHistogramData, + payload: {data: [{response}]}, }) export const executeHistogramQueryAsync = () => async ( @@ -90,36 +149,51 @@ export const executeHistogramQueryAsync = () => async ( getState: GetState ): Promise => { const state = getState() - const queryConfig = getDeep( - state, - 'logs.histogramQueryConfig', - null - ) - const timeRange = getDeep(state, 'logs.timeRange', null) - const namespace = getDeep( - state, - 'logs.currentNamespace', - null - ) - const proxyLink = getDeep( - state, - 'logs.currentSource.links.proxy', - null - ) - if (queryConfig && timeRange && namespace && proxyLink) { + const queryConfig = getHistogramQueryConfig(state) + const timeRange = getTimeRange(state) + const namespace = getNamespace(state) + const proxyLink = getProxyLink(state) + + if (_.every([queryConfig, timeRange, namespace, proxyLink])) { const query = buildQuery(timeRange, queryConfig) const response = await executeQueryAsync(proxyLink, namespace, query) - dispatch({ - type: ActionTypes.SetHistogramData, - payload: { - data: [{response}], - }, - }) + dispatch(setHistogramData(response)) } } +const setTableData = (series: TableData): SetTableData => ({ + type: ActionTypes.SetTableData, + payload: {data: {columns: series.columns, values: _.reverse(series.values)}}, +}) + +export const executeTableQueryAsync = () => async ( + dispatch, + getState: GetState +): Promise => { + const state = getState() + + const queryConfig = getTableQueryConfig(state) + const timeRange = getTimeRange(state) + const namespace = getNamespace(state) + const proxyLink = getProxyLink(state) + + if (_.every([queryConfig, timeRange, namespace, proxyLink])) { + const query = buildQuery(timeRange, queryConfig) + const response = await executeQueryAsync(proxyLink, namespace, query) + + const series = getDeep(response, 'results.0.series.0', defaultTableData) + + dispatch(setTableData(series)) + } +} + +export const executeQueriesAsync = () => async dispatch => { + dispatch(executeHistogramQueryAsync()) + dispatch(executeTableQueryAsync()) +} + export const setHistogramQueryConfigAsync = () => async ( dispatch, getState: GetState @@ -144,6 +218,31 @@ export const setHistogramQueryConfigAsync = () => async ( } } +export const setTableQueryConfig = (queryConfig: QueryConfig) => ({ + type: ActionTypes.SetTableQueryConfig, + payload: {queryConfig}, +}) + +export const setTableQueryConfigAsync = () => async ( + dispatch, + getState: GetState +): Promise => { + const state = getState() + const namespace = getDeep( + state, + 'logs.currentNamespace', + null + ) + const timeRange = getDeep(state, 'logs.timeRange', null) + + if (timeRange && namespace) { + const queryConfig = buildTableQueryConfig(namespace, timeRange) + + dispatch(setTableQueryConfig(queryConfig)) + dispatch(executeTableQueryAsync()) + } +} + export const setNamespaceAsync = (namespace: Namespace) => async ( dispatch ): Promise => { @@ -153,6 +252,7 @@ export const setNamespaceAsync = (namespace: Namespace) => async ( }) dispatch(setHistogramQueryConfigAsync()) + dispatch(setTableQueryConfigAsync()) } export const setNamespaces = ( @@ -174,6 +274,7 @@ export const setTimeRangeAsync = (timeRange: TimeRange) => async ( }, }) dispatch(setHistogramQueryConfigAsync()) + dispatch(setTableQueryConfigAsync()) } export const populateNamespacesAsync = (proxyLink: string) => async ( @@ -206,16 +307,9 @@ export const changeZoomAsync = (timeRange: TimeRange) => async ( getState: GetState ): Promise => { const state = getState() - const namespace = getDeep( - state, - 'logs.currentNamespace', - null - ) - const proxyLink = getDeep( - state, - 'logs.currentSource.links.proxy', - null - ) + + const namespace = getNamespace(state) + const proxyLink = getProxyLink(state) if (namespace && proxyLink) { const queryConfig = buildHistogramQueryConfig(namespace, timeRange) @@ -229,5 +323,8 @@ export const changeZoomAsync = (timeRange: TimeRange) => async ( timeRange, }, }) + + await dispatch(setTimeRangeAsync(timeRange)) + await dispatch(executeTableQueryAsync()) } } diff --git a/ui/src/logs/components/LogsTable.tsx b/ui/src/logs/components/LogsTable.tsx index ef6d52efe..d5ee98218 100644 --- a/ui/src/logs/components/LogsTable.tsx +++ b/ui/src/logs/components/LogsTable.tsx @@ -1,17 +1,178 @@ +import moment from 'moment' import React, {PureComponent} from 'react' +import {Grid, AutoSizer} from 'react-virtualized' +import {getDeep} from 'src/utils/wrappers' +import FancyScrollbar from 'src/shared/components/FancyScrollbar' interface Props { - thing: string + data: { + columns: string[] + values: string[] + } } -class LogsTableContainer extends PureComponent { +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', +] + +class LogsTable extends PureComponent { public render() { + const rowCount = getDeep(this.props, 'data.values.length', 0) + const columnCount = getDeep(this.props, 'data.columns.length', 1) - 1 + return (
-

{this.props.thing}

+ + {({width}) => ( + + )} + + + {({width, height}) => ( + + + + )} + +
+ ) + } + + private severityLevel(value: number): string { + switch (value) { + case 0: + return 'Emergency' + case 1: + return 'Alert' + case 2: + return 'Critical' + case 3: + return 'Error' + case 4: + return 'Warning' + case 5: + return 'Notice' + case 6: + return 'Informational' + default: + return 'Debug' + } + } + + private getColumnWidth = ({index}: {index: number}) => { + const column = getDeep(this.props, `data.columns.${index + 1}`, '') + + switch (column) { + case 'message': + return 700 + case 'timestamp': + return 400 + default: + return 200 + } + } + + private header(key: string): string { + return getDeep( + { + timestamp: 'Timestamp', + facility_code: 'Facility', + procid: 'Proc ID', + severity_code: 'Severity', + message: 'Message', + }, + key, + '' + ) + } + + private facility(key: number): string { + return getDeep(FACILITY_CODES, key, '') + } + + private headerRenderer = ({key, style, columnIndex}) => { + const value = getDeep( + this.props, + `data.columns.${columnIndex + 1}`, + '' + ) + + return ( +
+ {this.header(value)} +
+ ) + } + + private cellRenderer = ({key, style, rowIndex, columnIndex}) => { + const column = getDeep( + this.props, + `data.columns.${columnIndex + 1}`, + '' + ) + + let value = this.props.data.values[rowIndex][columnIndex + 1] + + switch (column) { + 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) + break + } + + return ( +
+ {value}
) } } -export default LogsTableContainer +export default LogsTable diff --git a/ui/src/logs/components/TimeRangeDropdown.tsx b/ui/src/logs/components/TimeRangeDropdown.tsx index c3ad299f1..ef48dd5cc 100644 --- a/ui/src/logs/components/TimeRangeDropdown.tsx +++ b/ui/src/logs/components/TimeRangeDropdown.tsx @@ -130,10 +130,9 @@ class TimeRangeDropdown extends Component { } private get dropdownClassName(): string { - const { - isOpen, - customTimeRange: {lower, upper}, - } = this.state + const {isOpen} = this.state + + const {lower, upper} = _.get(this.props, 'selected', {upper: '', lower: ''}) const absoluteTimeRange = !_.isEmpty(lower) && !_.isEmpty(upper) diff --git a/ui/src/logs/containers/LogsPage.tsx b/ui/src/logs/containers/LogsPage.tsx index 6ef3656d8..9a36fb118 100644 --- a/ui/src/logs/containers/LogsPage.tsx +++ b/ui/src/logs/containers/LogsPage.tsx @@ -4,16 +4,17 @@ import { getSourceAndPopulateNamespacesAsync, setTimeRangeAsync, setNamespaceAsync, - executeHistogramQueryAsync, + executeQueriesAsync, changeZoomAsync, } from 'src/logs/actions' import {getSourcesAsync} from 'src/shared/actions/sources' import LogViewerHeader from 'src/logs/components/LogViewerHeader' import Graph from 'src/logs/components/LogsGraph' -import Table from 'src/logs/components/LogsTable' import SearchBar from 'src/logs/components/LogsSearchBar' import FilterBar from 'src/logs/components/LogsFilterBar' import LogViewerChart from 'src/logs/components/LogViewerChart' +import LogsTable from 'src/logs/components/LogsTable' +import {getDeep} from 'src/utils/wrappers' import {Source, Namespace, TimeRange} from 'src/types' @@ -35,9 +36,13 @@ interface Props { setTimeRangeAsync: (timeRange: TimeRange) => void setNamespaceAsync: (namespace: Namespace) => void changeZoomAsync: (timeRange: TimeRange) => void - executeHistogramQueryAsync: () => void + executeQueriesAsync: () => void timeRange: TimeRange histogramData: object[] + tableData: { + columns: string[] + values: string[] + } } interface State { @@ -73,11 +78,17 @@ class LogsPage extends PureComponent { public componentDidMount() { this.props.getSources() + + if (this.props.currentNamespace) { + this.props.executeQueriesAsync() + } } public render() { const {searchString, filters} = this.state + const count = getDeep(this.props, 'tableData.values.length', 0) + return (
{this.header} @@ -89,11 +100,11 @@ class LogsPage extends PureComponent { onSearch={this.handleSubmitSearch} /> - + ) @@ -149,7 +160,7 @@ class LogsPage extends PureComponent { private handleChooseTimerange = (timeRange: TimeRange) => { this.props.setTimeRangeAsync(timeRange) - this.props.executeHistogramQueryAsync() + this.props.executeQueriesAsync() } private handleChooseSource = (sourceID: string) => { @@ -175,6 +186,7 @@ const mapStateToProps = ({ timeRange, currentNamespace, histogramData, + tableData, }, }) => ({ sources, @@ -183,6 +195,7 @@ const mapStateToProps = ({ timeRange, currentNamespace, histogramData, + tableData, }) const mapDispatchToProps = { @@ -190,7 +203,7 @@ const mapDispatchToProps = { getSources: getSourcesAsync, setTimeRangeAsync, setNamespaceAsync, - executeHistogramQueryAsync, + executeQueriesAsync, changeZoomAsync, } diff --git a/ui/src/logs/reducers/index.ts b/ui/src/logs/reducers/index.ts index 28e0b4717..96484f008 100644 --- a/ui/src/logs/reducers/index.ts +++ b/ui/src/logs/reducers/index.ts @@ -7,6 +7,8 @@ const defaultState: LogsState = { timeRange: {lower: 'now() - 1m', upper: null}, currentNamespace: null, histogramQueryConfig: null, + tableQueryConfig: null, + tableData: [], histogramData: [], } @@ -24,6 +26,10 @@ export default (state: LogsState = defaultState, action: Action) => { return {...state, histogramQueryConfig: action.payload.queryConfig} case ActionTypes.SetHistogramData: return {...state, histogramData: action.payload.data} + case ActionTypes.SetTableQueryConfig: + return {...state, tableQueryConfig: action.payload.queryConfig} + case ActionTypes.SetTableData: + return {...state, tableData: action.payload.data} case ActionTypes.ChangeZoom: const {timeRange, data} = action.payload return {...state, timeRange, histogramData: data} diff --git a/ui/src/logs/utils/index.ts b/ui/src/logs/utils/index.ts index 6efd36f0f..270e591e7 100644 --- a/ui/src/logs/utils/index.ts +++ b/ui/src/logs/utils/index.ts @@ -1,7 +1,10 @@ +import moment from 'moment' import uuid from 'uuid' import {TimeRange, Namespace, QueryConfig} from 'src/types' -const fields = [ +const BIN_COUNT = 30 + +const histogramFields = [ { alias: '', args: [ @@ -16,27 +19,96 @@ const fields = [ }, ] +const tableFields = [ + { + alias: 'timestamp', + type: 'field', + value: 'timestamp', + }, + { + alias: 'facility_code', + type: 'field', + value: 'facility_code', + }, + { + alias: 'procid', + type: 'field', + value: 'procid', + }, + { + alias: 'severity_code', + type: 'field', + value: 'severity_code', + }, + { + alias: 'message', + type: 'field', + value: 'message', + }, +] + const defaultQueryConfig = { areTagsAccepted: false, - fields, fill: '0', - groupBy: {time: '2m', tags: []}, measurement: 'syslog', rawText: null, shifts: [], tags: {}, } +const computeSeconds = (range: TimeRange) => { + const {upper, lower, seconds} = range + + if (seconds) { + return seconds + } else if (upper && lower) { + return moment(upper).unix() - moment(lower).unix() + } else { + return 120 + } +} + +const createGroupBy = (range: TimeRange) => { + const seconds = computeSeconds(range) + const time = `${Math.floor(seconds / BIN_COUNT)}s` + const tags = [] + + return {time, tags} +} + export const buildHistogramQueryConfig = ( namespace: Namespace, range: TimeRange ): QueryConfig => { const id = uuid.v4() + const {database, retentionPolicy} = namespace + return { ...defaultQueryConfig, id, range, - database: namespace.database, - retentionPolicy: namespace.retentionPolicy, + database, + retentionPolicy, + groupBy: createGroupBy(range), + fields: histogramFields, + } +} + +export const buildTableQueryConfig = ( + namespace: Namespace, + range: TimeRange +): QueryConfig => { + const id = uuid.v4() + const {database, retentionPolicy} = namespace + + return { + ...defaultQueryConfig, + id, + range, + database, + retentionPolicy, + groupBy: {tags: []}, + fields: tableFields, + fill: null, } } diff --git a/ui/src/types/localStorage.ts b/ui/src/types/localStorage.ts index 26969be01..2b328b948 100644 --- a/ui/src/types/localStorage.ts +++ b/ui/src/types/localStorage.ts @@ -7,6 +7,8 @@ export interface LogsState { timeRange: TimeRange histogramQueryConfig: QueryConfig | null histogramData: object[] + tableQueryConfig: QueryConfig | null + tableData: object[] } export interface LocalStorage { diff --git a/ui/src/types/query.ts b/ui/src/types/query.ts index e3c850dfb..807bd7785 100644 --- a/ui/src/types/query.ts +++ b/ui/src/types/query.ts @@ -86,6 +86,7 @@ export interface Status { export interface TimeRange { lower: string upper?: string | null + seconds?: number } export interface DurationRange { diff --git a/ui/src/utils/wrappers.ts b/ui/src/utils/wrappers.ts index 8a64f0d76..8ed58963d 100644 --- a/ui/src/utils/wrappers.ts +++ b/ui/src/utils/wrappers.ts @@ -1,5 +1,9 @@ import _ from 'lodash' -export function getDeep(obj: any, path: string, fallback: T): T { +export function getDeep( + obj: any, + path: string | number, + fallback: T +): T { return _.get(obj, path, fallback) }